json -
read and write JSON formatted text
These services can be useful to communicate with a remote server or client, save data locally or even debug and understand the structure of a Nit object. There is a single API to write JSON, and three API to read depending on the use case.
Write JSON
Writing Nit objects to JSON format can be useful to communicate with a remote service,
save data locally or even debug and understand the structure of an object.
There is two related services to write JSON object, the method
serialize_to_json
and the object JsonSerializer
.
The method serialize_to_json
is actually a shortcut to JsonSerializer
, both
share the same features.
Write plain JSON
Passing the argument plain=true
to serialize_to_json
generates plain and clean JSON.
This format is non-Nit program, it cannot be fully deserialized back to Nit objects.
The argument pretty=true
generates JSON for humans, with more spaces and line breaks.
The Nit objects to write must subclass Serializable
and implement its services.
Most classes from the core
library are already supported, including collections, numeric values, etc.
For your local objects, you can annotate them with serialize
to automate subclassing
Serializable
and the implementation of its services.
Example
class Person
serialize
var name: String
var year_of_birth: Int
var next_of_kin: nullable Person
end
var bob = new Person("Bob", 1986)
assert bob.serialize_to_json(pretty=true, plain=true) == """
{
"name": "Bob",
"year_of_birth": 1986,
"next_of_kin": null
}"""
var alice = new Person("Alice", 1978, bob)
assert alice.serialize_to_json(pretty=true, plain=true) == """
{
"name": "Alice",
"year_of_birth": 1978,
"next_of_kin": {
"name": "Bob",
"year_of_birth": 1986,
"next_of_kin": null
}
}"""
# You can also build JSON objects as a `Map`
var charlie = new Map[String, nullable Serializable]
charlie["name"] = "Charlie"
charlie["year_of_birth"] = 1968
charlie["next_of_kin"] = alice
assert charlie.serialize_to_json(pretty=true, plain=true) == """
{
"name": "Charlie",
"year_of_birth": 1968,
"next_of_kin": {
"name": "Alice",
"year_of_birth": 1978,
"next_of_kin": {
"name": "Bob",
"year_of_birth": 1986,
"next_of_kin": null
}
}
}"""
Write JSON with metadata
By default, serialize_to_json
and JsonSerializer
include metadate in the generated JSON.
This metadata is used by JsonDeserializer
when reading the JSON code to recreate
the Nit object with the exact original type.
The metadata allows to avoid repeating an object and its resolves cycles in the serialized objects.
For more information on Nit serialization, see: ../serialization/README.md
Read JSON
There are a total of 3 API to read JSON:
JsonDeserializer
reads JSON to recreate complex Nit objects (discussed here),- the module
json::dynamic
provides an easy API to explore JSON objects, - the module
json::static
offers a low-level service to parse JSON and create basic Nit objects.
The class JsonDeserializer
reads JSON code to recreate objects.
It can use the metadata in the JSON code, to recreate precise Nit objects.
Otherwise, JSON objects are recreated to simple Nit types: Map
, Array
, etc.
Errors are reported to the attribute JsonDeserializer::errors
.
The type to recreate is either declared or inferred:
- The JSON object defines a
__class
key with the name of the Nit class as value. This attribute is generated by theJsonSerializer
with other metadata, it can also be specified by other external tools. - A refinement of
JsonDeserializer::class_name_heuristic
identifies the Nit class. - If all else fails,
JsonDeserializer
uses the static type of the attribute, or the type name passed todeserialize
.
The method deserialize_json
is a shortcut to JsonDeserializer
which prints
errors to the console. It is fit only for small scripts and other quick and dirty usage.
Example
class Triangle
serialize
var corners = new Array[Point]
redef var to_s is serialize_as("name")
end
class Point
serialize
var x: Int
var y: Int
end
# Metadata on each JSON object tells the deserializer what is its Nit type,
# and it supports special types such as generic collections.
var json_with_metadata = """{
"__class": "Triangle",
"corners": {"__class": "Array[Point]",
"__items": [{"__class": "Point", "x": 0, "y": 0},
{"__class": "Point", "x": 3, "y": 0},
{"__class": "Point", "x": 2, "y": 2}]},
"name": "some triangle"
}"""
var deserializer = new JsonDeserializer(json_with_metadata)
var object = deserializer.deserialize
assert deserializer.errors.is_empty
assert object != null
# However most non-Nit services won't add the metadata and instead produce plain JSON.
# Without a "__class", the deserializer relies on `class_name_heuristic` and the static type.
# The type of the root object to deserialize can be specified by an argument passed to `deserialize`.
var plain_json = """{
"corners": [{"x": 0, "y": 0},
{"x": 3, "y": 0},
{"x": 2, "y": 2}],
"name": "the same triangle"
}"""
deserializer = new JsonDeserializer(plain_json)
object = deserializer.deserialize("Triangle")
assert deserializer.errors.is_empty
Missing attributes and default values
When reading JSON, some attributes expected by Nit classes may be missing. The JSON object may come from an external API using optional attributes or from a previous version of your program without the attributes. When an attribute is not found, the deserialization engine acts in one of three ways:
- If the attribute has a default value or if it is annotated by
lazy
, the engine leave the attribute to the default value. No error is raised. - If the static type of the attribute is nullable, the engine sets
the attribute to
null
. No error is raised. - Otherwise, the engine raises an error and does not set the attribute.
The caller must check for
errors
and must not read from the attribute.
class MyConfig
serialize
var width: Int # Must be in JSON or an error is raised
var height = 4
var volume_level = 8 is lazy
var player_name: nullable String
var tmp_dir: nullable String = "/tmp" is lazy
end
# ---
# JSON object with all expected attributes -> OK
var plain_json = """
{
"width": 11,
"height": 22,
"volume_level": 33,
"player_name": "Alice",
"tmp_dir": null
}"""
var deserializer = new JsonDeserializer(plain_json)
var obj = deserializer.deserialize("MyConfig")
assert deserializer.errors.is_empty
assert obj isa MyConfig
assert obj.width == 11
assert obj.height == 22
assert obj.volume_level == 33
assert obj.player_name == "Alice"
assert obj.tmp_dir == null
# ---
# JSON object missing optional attributes -> OK
plain_json = """
{
"width": 11
}"""
deserializer = new JsonDeserializer(plain_json)
obj = deserializer.deserialize("MyConfig")
assert deserializer.errors.is_empty
assert obj isa MyConfig
assert obj.width == 11
assert obj.height == 4
assert obj.volume_level == 8
assert obj.player_name == null
assert obj.tmp_dir == "/tmp"
# ---
# JSON object missing the mandatory attribute -> Error
plain_json = """
{
"player_name": "Bob",
}"""
deserializer = new JsonDeserializer(plain_json)
obj = deserializer.deserialize("MyConfig")
# There's an error, `obj` is partial
assert deserializer.errors.length == 1
# Still, we can access valid attributes
assert obj isa MyConfig
assert obj.player_name == "Bob"
Content
- json: read and write JSON formatted text (lib/json)
- dynamic: Dynamic interface to read values from JSON strings (lib/json/dynamic.nit)
- error: Intro
JsonParseError
which is exposed by all JSON reading APIs (lib/json/error.nit) - json: Read and write JSON formatted text using the standard serialization services (lib/json/json.nit)
- serialization_read: Services to read JSON:
deserialize_json
andJsonDeserializer
(lib/json/serialization_read.nit) - serialization_write: Services to write Nit objects to JSON strings:
serialize_to_json
andJsonSerializer
(lib/json/serialization_write.nit) - static: Static interface to read Nit objects from JSON strings (lib/json/static.nit)
- store: Store and load json data. (lib/json/store.nit)