json: rename and improve the quick and easy `deserialize_json`
[nit.git] / lib / json / README.md
1 read and write JSON formatted text
2
3 These services can be useful to communicate with a remote server or client,
4 save data locally or even debug and understand the structure of a Nit object.
5 There is a single API to write JSON, and three API to read depending on the use case.
6
7 # Write JSON
8
9 Writing Nit objects to JSON format can be useful to communicate with a remote service,
10 save data locally or even debug and understand the structure of an object.
11 There is two related services to write JSON object, the method
12 `serialize_to_json` and the object `JsonSerializer`.
13 The method `serialize_to_json` is actually a shortcut to `JsonSerializer`, both
14 share the same features.
15
16 ## Write plain JSON
17
18 Passing the argument `plain=true` to `serialize_to_json` generates plain and clean JSON.
19 This format is non-Nit program, it cannot be fully deserialized back to Nit objects.
20 The argument `pretty=true` generates JSON for humans, with more spaces and line breaks.
21
22 The Nit objects to write must subclass `Serializable` and implement its services.
23 Most classes from the `core` library are already supported, including collections, numeric values, etc.
24 For your local objects, you can annotate them with `serialize` to automate subclassing
25 `Serializable` and the implementation of its services.
26
27 ### Example
28
29 ~~~
30 class Person
31     serialize
32
33     var name: String
34     var year_of_birth: Int
35     var next_of_kin: nullable Person
36 end
37
38 var bob = new Person("Bob", 1986)
39 assert bob.serialize_to_json(pretty=true, plain=true) == """
40 {
41         "name": "Bob",
42         "year_of_birth": 1986,
43         "next_of_kin": null
44 }"""
45
46 var alice = new Person("Alice", 1978, bob)
47 assert alice.serialize_to_json(pretty=true, plain=true) == """
48 {
49         "name": "Alice",
50         "year_of_birth": 1978,
51         "next_of_kin": {
52                 "name": "Bob",
53                 "year_of_birth": 1986,
54                 "next_of_kin": null
55         }
56 }"""
57
58 # You can also build JSON objects as a `Map`
59 var charlie = new Map[String, nullable Serializable]
60 charlie["name"] = "Charlie"
61 charlie["year_of_birth"] = 1968
62 charlie["next_of_kin"] = alice
63 assert charlie.serialize_to_json(pretty=true, plain=true) == """
64 {
65         "name": "Charlie",
66         "year_of_birth": 1968,
67         "next_of_kin": {
68                 "name": "Alice",
69                 "year_of_birth": 1978,
70                 "next_of_kin": {
71                         "name": "Bob",
72                         "year_of_birth": 1986,
73                         "next_of_kin": null
74                 }
75         }
76 }"""
77 ~~~
78
79 ## Write JSON with metadata
80
81 By default, `serialize_to_json` and `JsonSerializer` include metadate in the generated JSON.
82 This metadata is used by `JsonDeserializer` when reading the JSON code to recreate
83 the Nit object with the exact original type.
84 The metadata allows to avoid repeating an object and its resolves cycles in the serialized objects.
85
86 For more information on Nit serialization, see: ../serialization/README.md
87
88
89 # Read JSON
90
91 There are a total of 3 API to read JSON:
92 * `JsonDeserializer` reads JSON to recreate complex Nit objects (discussed here),
93 * the module `json::dynamic` provides an easy API to explore JSON objects,
94 * the module `json::static` offers a low-level service to parse JSON and create basic Nit objects.
95
96 The class `JsonDeserializer` reads JSON code to recreate objects.
97 It can use the metadata in the JSON code, to recreate precise Nit objects.
98 Otherwise, JSON objects are recreated to simple Nit types: `Map`, `Array`, etc.
99 Errors are reported to the attribute `JsonDeserializer::errors`.
100
101 The type to recreate is either declared or inferred:
102
103 1. The JSON object defines a `__class` key with the name of the Nit class as value.
104    This attribute is generated by the `JsonSerializer` with other metadata,
105    it can also be specified by other external tools.
106 2. A refinement of `JsonDeserializer::class_name_heuristic` identifies the Nit class.
107 3. If all else fails, `JsonDeserializer` uses the static type of the attribute,
108    or the type name passed to `deserialize`.
109
110 The method `deserialize_json` is a shortcut to `JsonDeserializer` which prints
111 errors to the console. It is fit only for small scripts and other quick and dirty usage.
112
113 ### Example
114
115 ~~~
116 class Triangle
117     serialize
118
119     var corners = new Array[Point]
120     redef var to_s is serialize_as("name")
121 end
122
123 class Point
124     serialize
125
126     var x: Int
127     var y: Int
128 end
129
130 # Metadata on each JSON object tells the deserializer what is its Nit type,
131 # and it supports special types such as generic collections.
132 var json_with_metadata = """{
133     "__class": "Triangle",
134     "corners": {"__class": "Array[Point]",
135                 "__items": [{"__class": "Point", "x": 0, "y": 0},
136                             {"__class": "Point", "x": 3, "y": 0},
137                             {"__class": "Point", "x": 2, "y": 2}]},
138     "name": "some triangle"
139 }"""
140
141 var deserializer = new JsonDeserializer(json_with_metadata)
142 var object = deserializer.deserialize
143 assert deserializer.errors.is_empty
144 assert object != null
145
146 # However most non-Nit services won't add the metadata and instead produce plain JSON.
147 # Without a "__class", the deserializer relies on `class_name_heuristic` and the static type.
148 # The type of the root object to deserialize can be specified by an argument passed to `deserialize`.
149 var plain_json = """{
150     "corners": [{"x": 0, "y": 0},
151                 {"x": 3, "y": 0},
152                 {"x": 2, "y": 2}],
153     "name": "the same triangle"
154 }"""
155
156 deserializer = new JsonDeserializer(plain_json)
157 object = deserializer.deserialize("Triangle")
158 assert deserializer.errors.is_empty # If false, `object` is invalid
159 ~~~
160
161 ### Missing attributes and default values
162
163 When reading JSON, some attributes expected by Nit classes may be missing.
164 The JSON object may come from an external API using optional attributes or
165 from a previous version of your program without the attributes.
166 When an attribute is not found, the deserialization engine acts in one of three ways:
167
168 1. If the attribute has a default value or if it is annotated by `lazy`,
169    the engine leave the attribute to the default value. No error is raised.
170 2. If the static type of the attribute is nullable, the engine sets
171    the attribute to `null`. No error is raised.
172 3. Otherwise, the engine raises an error and does not set the attribute.
173    The caller must check for `errors` and must not read from the attribute.
174
175 ~~~
176 class MyConfig
177     serialize
178
179     var width: Int # Must be in JSON or an error is raised
180     var height = 4
181     var volume_level = 8 is lazy
182     var player_name: nullable String
183     var tmp_dir: nullable String = "/tmp" is lazy
184 end
185
186 # ---
187 # JSON object with all expected attributes -> OK
188 var plain_json = """
189 {
190     "width": 11,
191     "height": 22,
192     "volume_level": 33,
193     "player_name": "Alice",
194     "tmp_dir": null
195 }"""
196 var deserializer = new JsonDeserializer(plain_json)
197 var obj = deserializer.deserialize("MyConfig")
198
199 assert deserializer.errors.is_empty
200 assert obj isa MyConfig
201 assert obj.width == 11
202 assert obj.height == 22
203 assert obj.volume_level == 33
204 assert obj.player_name == "Alice"
205 assert obj.tmp_dir == null
206
207 # ---
208 # JSON object missing optional attributes -> OK
209 plain_json = """
210 {
211     "width": 11
212 }"""
213 deserializer = new JsonDeserializer(plain_json)
214 obj = deserializer.deserialize("MyConfig")
215
216 assert deserializer.errors.is_empty
217 assert obj isa MyConfig
218 assert obj.width == 11
219 assert obj.height == 4
220 assert obj.volume_level == 8
221 assert obj.player_name == null
222 assert obj.tmp_dir == "/tmp"
223
224 # ---
225 # JSON object missing the mandatory attribute -> Error
226 plain_json = """
227 {
228     "player_name": "Bob",
229 }"""
230 deserializer = new JsonDeserializer(plain_json)
231 obj = deserializer.deserialize("MyConfig")
232
233 # There's an error, `obj` is partial
234 assert deserializer.errors.length == 1
235
236 # Still, we can access valid attributes
237 assert obj isa MyConfig
238 assert obj.player_name == "Bob"
239 ~~~