lib/json: add error checking in the json deserialization example
[nit.git] / lib / json / serialization.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Handles serialization and deserialization of objects to/from JSON
18 #
19 # ## Writing JSON with metadata
20 #
21 # `JsonSerializer` write Nit objects that subclass `Serializable` to JSON,
22 # and `JsonDeserializer` can read them. They both use metadata added to the
23 # generated JSON to recreate the Nit instances with the exact original type.
24 #
25 # For more information on Nit serialization, see: ../serialization/README.md
26 #
27 # ## Writing plain JSON
28 #
29 # The attribute `JsonSerializer::plain_json` triggers generating plain and
30 # clean JSON. This format is easier to read for an human and a non-Nit program,
31 # but it cannot be fully deserialized. It can still be read by services from
32 # `json::static` and `json::dynamic`.
33 #
34 # A shortcut to these writing services is provided by `Serializable::serialize_to_json`.
35 #
36 # ### Usage Example
37 #
38 # ~~~nitish
39 # import json::serialization
40 #
41 # class Person
42 # serialize
43 #
44 # var name: String
45 # var year_of_birth: Int
46 # var next_of_kin: nullable Person
47 # end
48 #
49 # var bob = new Person("Bob", 1986)
50 # var alice = new Person("Alice", 1978, bob)
51 #
52 # assert bob.serialize_to_json(pretty=true, plain=true) == """
53 #{
54 # "name": "Bob",
55 # "year_of_birth": 1986,
56 # "next_of_kin": null
57 #}"""
58 #
59 # assert alice.serialize_to_json(pretty=true, plain=true) == """
60 #{
61 # "name": "Alice",
62 # "year_of_birth": 1978,
63 # "next_of_kin": {
64 # "name": "Bob",
65 # "year_of_birth": 1986,
66 # "next_of_kin": null
67 # }
68 #}"""
69 # ~~~
70 #
71 # ## JSON to Nit objects
72 #
73 # The `JsonDeserializer` support reading JSON code with minimal metadata
74 # to easily create Nit object from client-side code or configuration files.
75 # Each JSON object must define the `__class` attribute with the corresponding
76 # Nit class and the expected attributes with its name in Nit followed by its value.
77 #
78 # ### Usage Example
79 #
80 # ~~~nitish
81 # import json::serialization
82 #
83 # class MeetupConfig
84 # serialize
85 #
86 # var description: String
87 # var max_participants: nullable Int
88 # var answers: Array[FlatString]
89 # end
90 #
91 # var json_code = """
92 # {"__class": "MeetupConfig", "description": "My Awesome Meetup", "max_participants": null, "answers": ["Pepperoni", "Chicken"]}"""
93 # var deserializer = new JsonDeserializer(json_code)
94 #
95 # var meet = deserializer.deserialize
96 #
97 # # Check for errors
98 # assert deserializer.errors.is_empty
99 #
100 # assert meet isa MeetupConfig
101 # assert meet.description == "My Awesome Meetup"
102 # assert meet.max_participants == null
103 # assert meet.answers == ["Pepperoni", "Chicken"]
104 # ~~~
105 module serialization
106
107 import ::serialization::caching
108 private import ::serialization::engine_tools
109 private import static
110
111 # Serializer of Nit objects to Json string.
112 class JsonSerializer
113 super CachingSerializer
114
115 # Target writing stream
116 var stream: Writer
117
118 # Write plain JSON? Standard JSON without metadata for deserialization
119 #
120 # If `false`, the default, serialize to support deserialization:
121 #
122 # * Write metadata, including the types of the serialized objects so they can
123 # be deserialized to their original form using `JsonDeserializer`.
124 # * Use references when an object has already been serialized so to not duplicate it.
125 # * Support cycles in references.
126 # * Preserve the Nit `Char` type as an object because it does not exist in JSON.
127 # * The generated JSON is standard and can be read by non-Nit programs.
128 # However, some Nit types are not represented by the simplest possible JSON representation.
129 # With the added metadata, it can be complex to read.
130 #
131 # If `true`, serialize for other programs:
132 #
133 # * Nit objects are serialized to pure and standard JSON so they can
134 # be easily read by non-Nit programs and humans.
135 # * Nit objects are serialized for every references, so they can be duplicated.
136 # It is easier to read but it creates a larger output.
137 # * Does not support cycles, will replace the problematic references by `null`.
138 # * Does not serialize the metadata needed to deserialize the objects
139 # back to regular Nit objects.
140 # * Keys of Nit `HashMap` are converted to their string representation using `to_s`.
141 var plain_json = false is writable
142
143 # Write pretty JSON for human eyes?
144 #
145 # Toggles skipping lines between attributes of an object and
146 # properly indent the written JSON.
147 var pretty_json = false is writable
148
149 # Current indentation level used for writing `pretty_json`
150 private var indent_level = 0
151
152 # List of the current open objects, the first is the main target of the serialization
153 #
154 # Used only when `plain_json == true` to detect cycles in serialization.
155 private var open_objects = new Array[Object]
156
157 # Has the first attribute of the current object already been serialized?
158 #
159 # Used only when `plain_json == true`.
160 private var first_attribute = false
161
162 redef fun serialize(object)
163 do
164 if object == null then
165 stream.write "null"
166 else
167 if plain_json then
168 for o in open_objects do
169 if object.is_same_serialized(o) then
170 # Cycle, can't be managed in plain json
171 warn "Cycle detected in serialized object, replacing reference with 'null'."
172 stream.write "null"
173 return
174 end
175 end
176
177 open_objects.add object
178 end
179
180 first_attribute = true
181 object.accept_json_serializer self
182 first_attribute = false
183
184 if plain_json then open_objects.pop
185 end
186 end
187
188 redef fun serialize_attribute(name, value)
189 do
190 if not plain_json or not first_attribute then
191 stream.write ","
192 first_attribute = false
193 end
194
195 new_line_and_indent
196 stream.write "\""
197 stream.write name
198 stream.write "\": "
199 super
200 end
201
202 redef fun serialize_reference(object)
203 do
204 if not plain_json and cache.has_object(object) then
205 # if already serialized, add local reference
206 var id = cache.id_for(object)
207 stream.write "\{"
208 indent_level += 1
209 new_line_and_indent
210 stream.write "\"__kind\": \"ref\", \"__id\": "
211 stream.write id.to_s
212 indent_level -= 1
213 new_line_and_indent
214 stream.write "\}"
215 else
216 # serialize here
217 serialize object
218 end
219 end
220
221 # Write a new line and indent it, only if `pretty_json`
222 private fun new_line_and_indent
223 do
224 if pretty_json then
225 stream.write "\n"
226 for i in indent_level.times do stream.write "\t"
227 end
228 end
229 end
230
231 # Deserializer from a Json string.
232 class JsonDeserializer
233 super CachingDeserializer
234
235 # Json text to deserialize from.
236 private var text: Text
237
238 # Root json object parsed from input text.
239 private var root: nullable Object is noinit
240
241 # Depth-first path in the serialized object tree.
242 private var path = new Array[Map[String, nullable Object]]
243
244 # Last encountered object reference id.
245 #
246 # See `id_to_object`.
247 var just_opened_id: nullable Int = null
248
249 init do
250 var root = text.parse_json
251 if root isa Map[String, nullable Object] then path.add(root)
252 self.root = root
253 end
254
255 redef fun deserialize_attribute(name)
256 do
257 assert not path.is_empty # This is an internal error, abort
258 var current = path.last
259
260 if not current.keys.has(name) then
261 errors.add new Error("Deserialization Error: JSON object has not attribute '{name}'.")
262 return null
263 end
264
265 var value = current[name]
266
267 return convert_object(value)
268 end
269
270 # This may be called multiple times by the same object from constructors
271 # in different nclassdef
272 redef fun notify_of_creation(new_object)
273 do
274 var id = just_opened_id
275 if id == null then return # Register `new_object` only once
276 cache[id] = new_object
277 end
278
279 # Convert from simple Json object to Nit object
280 private fun convert_object(object: nullable Object): nullable Object
281 do
282 if object isa JsonParseError then
283 errors.add object
284 return null
285 end
286
287 if object isa Map[String, nullable Object] then
288 var kind = null
289 if object.keys.has("__kind") then
290 kind = object["__kind"]
291 end
292
293 # ref?
294 if kind == "ref" then
295 if not object.keys.has("__id") then
296 errors.add new Error("Serialization Error: JSON object reference does not declare a `__id`.")
297 return object
298 end
299
300 var id = object["__id"]
301 if not id isa Int then
302 errors.add new Error("Serialization Error: JSON object reference declares a non-integer `__id`.")
303 return object
304 end
305
306 if not cache.has_id(id) then
307 errors.add new Error("Serialization Error: JSON object reference has an unknown `__id`.")
308 return object
309 end
310
311 return cache.object_for(id)
312 end
313
314 # obj?
315 if kind == "obj" or kind == null then
316 var id = null
317 if object.keys.has("__id") then
318 id = object["__id"]
319
320 if not id isa Int then
321 errors.add new Error("Serialization Error: JSON object declaration declares a non-integer `__id`.")
322 return object
323 end
324
325 if cache.has_id(id) then
326 errors.add new Error("Serialization Error: JSON object with `__id` {id} is deserialized twice.")
327 # Keep going
328 end
329 end
330
331 var class_name = object.get_or_null("__class")
332 if class_name == null then
333 # Fallback to custom heuristic
334 class_name = class_name_heuristic(object)
335 end
336
337 if class_name == null then
338 errors.add new Error("Serialization Error: JSON object declaration does not declare a `__class`.")
339 return object
340 end
341
342 if not class_name isa String then
343 errors.add new Error("Serialization Error: JSON object declaration declares a non-string `__class`.")
344 return object
345 end
346
347 # advance on path
348 path.push object
349
350 just_opened_id = id
351 var value = deserialize_class(class_name)
352 just_opened_id = null
353
354 # revert on path
355 path.pop
356
357 return value
358 end
359
360 # char?
361 if kind == "char" then
362 if not object.keys.has("__val") then
363 errors.add new Error("Serialization Error: JSON `char` object does not declare a `__val`.")
364 return object
365 end
366
367 var val = object["__val"]
368
369 if not val isa String or val.is_empty then
370 errors.add new Error("Serialization Error: JSON `char` object does not declare a single char in `__val`.")
371 return object
372 end
373
374 return val.chars.first
375 end
376
377 errors.add new Error("Serialization Error: JSON object has an unknown `__kind`.")
378 return object
379 end
380
381 # Simple JSON array without serialization metadata
382 if object isa Array[nullable Object] then
383 var array = new Array[nullable Object]
384 var types = new HashSet[String]
385 var has_nullable = false
386 for e in object do
387 var res = convert_object(e)
388 array.add res
389
390 if res != null then
391 types.add res.class_name
392 else has_nullable = true
393 end
394
395 if types.length == 1 then
396 var array_type = types.first
397
398 var typed_array
399 if array_type == "ASCIIFlatString" or array_type == "UnicodeFlatString" then
400 if has_nullable then
401 typed_array = new Array[nullable FlatString]
402 else typed_array = new Array[FlatString]
403 else if array_type == "Int" then
404 if has_nullable then
405 typed_array = new Array[nullable Int]
406 else typed_array = new Array[Int]
407 else if array_type == "Float" then
408 if has_nullable then
409 typed_array = new Array[nullable Float]
410 else typed_array = new Array[Float]
411 else
412 # TODO support all array types when we separate the constructor
413 # `from_deserializer` from the filling of the items.
414
415 if not has_nullable then
416 typed_array = new Array[Object]
417 else
418 # Unsupported array type, return as `Array[nullable Object]`
419 return array
420 end
421 end
422
423 assert typed_array isa Array[nullable Object]
424
425 # Copy item to the new array
426 for e in array do typed_array.add e
427 return typed_array
428 end
429
430 # Uninferable type, return as `Array[nullable Object]`
431 return array
432 end
433
434 return object
435 end
436
437 redef fun deserialize
438 do
439 errors.clear
440 return convert_object(root)
441 end
442
443 # User customizable heuristic to get the name of the Nit class to deserialize `json_object`
444 #
445 # This method is called only when deserializing an object without the metadata `__class`.
446 # Return the class name as a `String` when it can be inferred.
447 # Return `null` when the class name cannot be found.
448 #
449 # If a valid class name is returned, `json_object` will then be deserialized normally.
450 # So it must contain the attributes of the corresponding class, as usual.
451 #
452 # ~~~nitish
453 # class MyData
454 # serialize
455 #
456 # var data: String
457 # end
458 #
459 # class MyError
460 # serialize
461 #
462 # var error: String
463 # end
464 #
465 # class MyJsonDeserializer
466 # super JsonDeserializer
467 #
468 # redef fun class_name_heuristic(json_object)
469 # do
470 # if json_object.keys.has("error") then return "MyError"
471 # if json_object.keys.has("data") then return "MyData"
472 # return null
473 # end
474 # end
475 #
476 # var json = """{"data": "some other data"}"""
477 # var deserializer = new MyJsonDeserializer(json)
478 # var deserialized = deserializer.deserialize
479 # assert deserialized isa MyData
480 #
481 # json = """{"error": "some error message"}"""
482 # deserializer = new MyJsonDeserializer(json)
483 # deserialized = deserializer.deserialize
484 # assert deserialized isa MyError
485 # ~~~
486 protected fun class_name_heuristic(json_object: Map[String, nullable Object]): nullable String
487 do
488 return null
489 end
490 end
491
492 redef class Text
493
494 # Deserialize a `nullable Object` from this JSON formatted string
495 #
496 # Warning: Deserialization errors are reported with `print_error` and
497 # may be returned as a partial object or as `null`.
498 #
499 # This method is not appropriate when errors need to be handled programmatically,
500 # manually use a `JsonDeserializer` in such cases.
501 fun from_json_string: nullable Object
502 do
503 var deserializer = new JsonDeserializer(self)
504 var res = deserializer.deserialize
505 if deserializer.errors.not_empty then
506 print_error "Deserialization Errors: {deserializer.errors.join(", ")}"
507 end
508 return res
509 end
510
511 redef fun accept_json_serializer(v) do v.stream.write(to_json)
512 end
513
514 redef class Serializable
515
516 # Serialize `self` to JSON
517 #
518 # Set `plain = true` to generate standard JSON, without deserialization metadata.
519 # Use this option if the generated JSON will be read by other programs or humans.
520 # Use the default, `plain = false`, if the JSON is to be deserialized by a Nit program.
521 #
522 # Set `pretty = true` to generate pretty JSON for human eyes.
523 # Use the default, `pretty = false`, to generate minified JSON.
524 #
525 # This method should not be refined by subclasses,
526 # instead `accept_json_serializer` can customize the serialization of an object.
527 #
528 # See: `JsonSerializer`
529 fun serialize_to_json(plain, pretty: nullable Bool): String
530 do
531 var stream = new StringWriter
532 var serializer = new JsonSerializer(stream)
533 serializer.plain_json = plain or else false
534 serializer.pretty_json = pretty or else false
535 serializer.serialize self
536 stream.close
537 return stream.to_s
538 end
539
540 # Refinable service to customize the serialization of this class to JSON
541 #
542 # This method can be refined to customize the serialization by either
543 # writing pure JSON directly on the stream `v.stream` or
544 # by using other services of `JsonSerializer`.
545 #
546 # Most of the time, it is preferable to refine the method `core_serialize_to`
547 # which is used by all the serialization engines, not just JSON.
548 protected fun accept_json_serializer(v: JsonSerializer)
549 do
550 var id = v.cache.new_id_for(self)
551 v.stream.write "\{"
552 v.indent_level += 1
553 if not v.plain_json then
554 v.new_line_and_indent
555 v.stream.write "\"__kind\": \"obj\", \"__id\": "
556 v.stream.write id.to_s
557 v.stream.write ", \"__class\": \""
558 v.stream.write class_name
559 v.stream.write "\""
560 end
561 core_serialize_to(v)
562
563 v.indent_level -= 1
564 v.new_line_and_indent
565 v.stream.write "\}"
566 end
567 end
568
569 redef class Int
570 redef fun accept_json_serializer(v) do v.stream.write to_s
571 end
572
573 redef class Float
574 redef fun accept_json_serializer(v) do v.stream.write to_s
575 end
576
577 redef class Bool
578 redef fun accept_json_serializer(v) do v.stream.write to_s
579 end
580
581 redef class Char
582 redef fun accept_json_serializer(v)
583 do
584 if v.plain_json then
585 to_s.accept_json_serializer v
586 else
587 v.stream.write "\{\"__kind\": \"char\", \"__val\": "
588 to_s.accept_json_serializer v
589 v.stream.write "\}"
590 end
591 end
592 end
593
594 redef class NativeString
595 redef fun accept_json_serializer(v) do to_s.accept_json_serializer(v)
596 end
597
598 redef class Collection[E]
599 # Utility to serialize a normal Json array
600 private fun serialize_to_pure_json(v: JsonSerializer)
601 do
602 v.stream.write "["
603 v.indent_level += 1
604 var is_first = true
605 for e in self do
606 if is_first then
607 is_first = false
608 else v.stream.write ","
609 v.new_line_and_indent
610
611 if not v.try_to_serialize(e) then
612 assert e != null # null would have been serialized
613 v.warn("element of type {e.class_name} is not serializable.")
614 end
615 end
616 v.indent_level -= 1
617 v.new_line_and_indent
618 v.stream.write "]"
619 end
620 end
621
622 redef class SimpleCollection[E]
623 redef fun accept_json_serializer(v)
624 do
625 # Register as pseudo object
626 if not v.plain_json then
627 var id = v.cache.new_id_for(self)
628 v.stream.write """{"""
629 v.indent_level += 1
630 v.new_line_and_indent
631 v.stream.write """"__kind": "obj", "__id": """
632 v.stream.write id.to_s
633 v.stream.write """, "__class": """"
634 v.stream.write class_name
635 v.stream.write """","""
636 v.new_line_and_indent
637 v.stream.write """"__items": """
638 end
639
640 serialize_to_pure_json v
641
642 if not v.plain_json then
643 v.indent_level -= 1
644 v.new_line_and_indent
645 v.stream.write "\}"
646 end
647 end
648
649 redef init from_deserializer(v)
650 do
651 super
652 if v isa JsonDeserializer then
653 v.notify_of_creation self
654 init
655
656 var arr = v.path.last["__items"].as(SequenceRead[nullable Object])
657 for o in arr do
658 var obj = v.convert_object(o)
659 self.add obj
660 end
661 end
662 end
663 end
664
665 redef class Map[K, V]
666 redef fun accept_json_serializer(v)
667 do
668 # Register as pseudo object
669 var id = v.cache.new_id_for(self)
670
671 if v.plain_json then
672 v.stream.write "\{"
673 v.indent_level += 1
674 var first = true
675 for key, val in self do
676 if not first then
677 v.stream.write ","
678 else first = false
679 v.new_line_and_indent
680
681 var k = key or else "null"
682 k.to_s.accept_json_serializer v
683 v.stream.write ": "
684 if not v.try_to_serialize(val) then
685 assert val != null # null would have been serialized
686 v.warn("element of type {val.class_name} is not serializable.")
687 v.stream.write "null"
688 end
689 end
690 v.indent_level -= 1
691 v.new_line_and_indent
692 v.stream.write "\}"
693 else
694 v.stream.write "\{"
695 v.indent_level += 1
696 v.new_line_and_indent
697 v.stream.write """"__kind": "obj", "__id": """
698 v.stream.write id.to_s
699 v.stream.write """, "__class": """"
700 v.stream.write class_name
701 v.stream.write """", "__length": """
702 v.stream.write length.to_s
703
704 v.stream.write ","
705 v.new_line_and_indent
706 v.stream.write """"__keys": """
707 keys.serialize_to_pure_json v
708
709 v.stream.write ","
710 v.new_line_and_indent
711 v.stream.write """"__values": """
712 values.serialize_to_pure_json v
713
714 v.indent_level -= 1
715 v.new_line_and_indent
716 v.stream.write "\}"
717 end
718 end
719
720 redef init from_deserializer(v)
721 do
722 super
723
724 if v isa JsonDeserializer then
725 v.notify_of_creation self
726 init
727
728 var length = v.deserialize_attribute("__length").as(Int)
729 var keys = v.path.last["__keys"].as(SequenceRead[nullable Object])
730 var values = v.path.last["__values"].as(SequenceRead[nullable Object])
731 for i in length.times do
732 var key = v.convert_object(keys[i])
733 var value = v.convert_object(values[i])
734 self[key] = value
735 end
736 end
737 end
738 end