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