X-Git-Url: http://nitlanguage.org diff --git a/lib/json/serialization.nit b/lib/json/serialization.nit index 813c094..45578f2 100644 --- a/lib/json/serialization.nit +++ b/lib/json/serialization.nit @@ -16,22 +16,22 @@ # Handles serialization and deserialization of objects to/from JSON # -# ## Nity JSON +# ## Writing JSON with metadata # # `JsonSerializer` write Nit objects that subclass `Serializable` to JSON, -# and `JsonDeserializer` can read them. They both use meta-data added to the +# and `JsonDeserializer` can read them. They both use metadata added to the # generated JSON to recreate the Nit instances with the exact original type. # # For more information on Nit serialization, see: ../serialization/README.md # -# ## Plain JSON +# ## Writing plain JSON # # The attribute `JsonSerializer::plain_json` triggers generating plain and # clean JSON. This format is easier to read for an human and a non-Nit program, # but it cannot be fully deserialized. It can still be read by services from # `json::static` and `json::dynamic`. # -# A shortcut to this service is provided by `Serializable::to_plain_json`. +# A shortcut to these writing services is provided by `Serializable::serialize_to_json`. # # ### Usage Example # @@ -39,7 +39,7 @@ # import json::serialization # # class Person -# auto_serializable +# serialize # # var name: String # var year_of_birth: Int @@ -49,37 +49,85 @@ # var bob = new Person("Bob", 1986) # var alice = new Person("Alice", 1978, bob) # -# assert bob.to_plain_json == """ -# {"name": "Bob", "year_of_birth": 1986, "next_of_kin": null}""" +# assert bob.serialize_to_json(pretty=true, plain=true) == """ +#{ +# "name": "Bob", +# "year_of_birth": 1986, +# "next_of_kin": null +#}""" # -# assert alice.to_plain_json == """ -# {"name": "Alice", "year_of_birth": 1978, "next_of_kin": {"name": "Bob", "year_of_birth": 1986, "next_of_kin": null}}""" +# 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 +# } +#}""" +# ~~~ +# +# ## JSON to Nit objects +# +# The `JsonDeserializer` support reading JSON code with minimal metadata +# to easily create Nit object from client-side code or configuration files. +# Each JSON object must define the `__class` attribute with the corresponding +# Nit class and the expected attributes with its name in Nit followed by its value. +# +# ### Usage Example +# +# ~~~nitish +# import json::serialization +# +# class MeetupConfig +# serialize +# +# var description: String +# var max_participants: nullable Int +# var answers: Array[FlatString] +# end +# +# var json_code = """ +# {"__class": "MeetupConfig", "description": "My Awesome Meetup", "max_participants": null, "answers": ["Pepperoni", "Chicken"]}""" +# var deserializer = new JsonDeserializer(json_code) +# +# var meet = deserializer.deserialize +# +# # Check for errors +# assert deserializer.errors.is_empty +# +# assert meet isa MeetupConfig +# assert meet.description == "My Awesome Meetup" +# assert meet.max_participants == null +# assert meet.answers == ["Pepperoni", "Chicken"] # ~~~ module serialization -import ::serialization +import ::serialization::caching private import ::serialization::engine_tools private import static +private import string_parser # Serializer of Nit objects to Json string. class JsonSerializer - super Serializer + super CachingSerializer # Target writing stream var stream: Writer - # Write plain JSON? easier to read but does not support Nit deserialization + # Write plain JSON? Standard JSON without metadata for deserialization # # If `false`, the default, serialize to support deserialization: # - # * Write meta-data, including the types of the serialized objects so they can + # * Write metadata, including the types of the serialized objects so they can # be deserialized to their original form using `JsonDeserializer`. # * Use references when an object has already been serialized so to not duplicate it. # * Support cycles in references. # * Preserve the Nit `Char` type as an object because it does not exist in JSON. # * The generated JSON is standard and can be read by non-Nit programs. # However, some Nit types are not represented by the simplest possible JSON representation. - # With the added meta-data, it can be complex to read. + # With the added metadata, it can be complex to read. # # If `true`, serialize for other programs: # @@ -88,11 +136,20 @@ class JsonSerializer # * Nit objects are serialized for every references, so they can be duplicated. # It is easier to read but it creates a larger output. # * Does not support cycles, will replace the problematic references by `null`. - # * Does not serialize the meta-data needed to deserialize the objects + # * Does not serialize the metadata needed to deserialize the objects # back to regular Nit objects. - # * Keys of Nit `HashMap` are converted to their string reprensentation using `to_s`. + # * Keys of Nit `HashMap` are converted to their string representation using `to_s`. var plain_json = false is writable + # Write pretty JSON for human eyes? + # + # Toggles skipping lines between attributes of an object and + # properly indent the written JSON. + var pretty_json = false is writable + + # Current indentation level used for writing `pretty_json` + private var indent_level = 0 + # List of the current open objects, the first is the main target of the serialization # # Used only when `plain_json == true` to detect cycles in serialization. @@ -111,7 +168,8 @@ class JsonSerializer if plain_json then for o in open_objects do if object.is_same_serialized(o) then - # Cycle detected + # Cycle, can't be managed in plain json + warn "Cycle detected in serialized object, replacing reference with 'null'." stream.write "null" return end @@ -121,7 +179,7 @@ class JsonSerializer end first_attribute = true - object.serialize_to_json self + object.accept_json_serializer self first_attribute = false if plain_json then open_objects.pop @@ -131,10 +189,11 @@ class JsonSerializer redef fun serialize_attribute(name, value) do if not plain_json or not first_attribute then - stream.write ", " + stream.write "," first_attribute = false end + new_line_and_indent stream.write "\"" stream.write name stream.write "\": " @@ -143,11 +202,16 @@ class JsonSerializer redef fun serialize_reference(object) do - if not plain_json and refs_map.has_key(object) then + if not plain_json and cache.has_object(object) then # if already serialized, add local reference - var id = ref_id_for(object) - stream.write "\{\"__kind\": \"ref\", \"__id\": " + var id = cache.id_for(object) + stream.write "\{" + indent_level += 1 + new_line_and_indent + stream.write "\"__kind\": \"ref\", \"__id\": " stream.write id.to_s + indent_level -= 1 + new_line_and_indent stream.write "\}" else # serialize here @@ -155,37 +219,28 @@ class JsonSerializer end end - # Map of references to already serialized objects. - private var refs_map = new StrictHashMap[Serializable,Int] - - # Get the internal serialized reference for this `object`. - private fun ref_id_for(object: Serializable): Int + # Write a new line and indent it, only if `pretty_json` + private fun new_line_and_indent do - if refs_map.has_key(object) then - return refs_map[object] - else - var id = refs_map.length - refs_map[object] = id - return id + if pretty_json then + stream.write "\n" + for i in indent_level.times do stream.write "\t" end end end # Deserializer from a Json string. class JsonDeserializer - super Deserializer + super CachingDeserializer # Json text to deserialize from. private var text: Text # Root json object parsed from input text. - private var root: nullable Jsonable is noinit + private var root: nullable Object is noinit # Depth-first path in the serialized object tree. - private var path = new Array[JsonObject] - - # Map of references to already deserialized objects. - private var id_to_object = new StrictHashMap[Int, Object] + private var path = new Array[Map[String, nullable Object]] # Last encountered object reference id. # @@ -194,16 +249,20 @@ class JsonDeserializer init do var root = text.parse_json - if root isa JsonObject then path.add(root) + if root isa Map[String, nullable Object] then path.add(root) self.root = root end redef fun deserialize_attribute(name) do - assert not path.is_empty + assert not path.is_empty # This is an internal error, abort var current = path.last - assert current.keys.has(name) + if not current.keys.has(name) then + errors.add new Error("Deserialization Error: JSON object has not attribute '{name}'.") + return null + end + var value = current[name] return convert_object(value) @@ -215,37 +274,76 @@ class JsonDeserializer do var id = just_opened_id if id == null then return # Register `new_object` only once - id_to_object[id] = new_object + cache[id] = new_object end # Convert from simple Json object to Nit object private fun convert_object(object: nullable Object): nullable Object do - if object isa JsonObject then - assert object.keys.has("__kind") - var kind = object["__kind"] + if object isa JsonParseError then + errors.add object + return null + end + + if object isa Map[String, nullable Object] then + var kind = null + if object.keys.has("__kind") then + kind = object["__kind"] + end # ref? if kind == "ref" then - assert object.keys.has("__id") + if not object.keys.has("__id") then + errors.add new Error("Serialization Error: JSON object reference does not declare a `__id`.") + return object + end + var id = object["__id"] - assert id isa Int + if not id isa Int then + errors.add new Error("Serialization Error: JSON object reference declares a non-integer `__id`.") + return object + end - assert id_to_object.has_key(id) - return id_to_object[id] + if not cache.has_id(id) then + errors.add new Error("Serialization Error: JSON object reference has an unknown `__id`.") + return object + end + + return cache.object_for(id) end # obj? - if kind == "obj" then - assert object.keys.has("__id") - var id = object["__id"] - assert id isa Int + if kind == "obj" or kind == null then + var id = null + if object.keys.has("__id") then + id = object["__id"] + + if not id isa Int then + errors.add new Error("Serialization Error: JSON object declaration declares a non-integer `__id`.") + return object + end + + if cache.has_id(id) then + errors.add new Error("Serialization Error: JSON object with `__id` {id} is deserialized twice.") + # Keep going + end + end - assert object.keys.has("__class") - var class_name = object["__class"] - assert class_name isa String + var class_name = object.get_or_null("__class") + if class_name == null then + # Fallback to custom heuristic + class_name = class_name_heuristic(object) + end + + if class_name == null then + errors.add new Error("Serialization Error: JSON object declaration does not declare a `__class`.") + return object + end - assert not id_to_object.has_key(id) else print "Error: Object with id '{id}' of {class_name} is deserialized twice." + if not class_name isa String then + errors.add new Error("Serialization Error: JSON object declaration declares a non-string `__class`.") + return object + end # advance on path path.push object @@ -262,38 +360,199 @@ class JsonDeserializer # char? if kind == "char" then - assert object.keys.has("__val") + if not object.keys.has("__val") then + errors.add new Error("Serialization Error: JSON `char` object does not declare a `__val`.") + return object + end + var val = object["__val"] - assert val isa String - if val.length != 1 then print "Error: expected a single char when deserializing '{val}'." + if not val isa String or val.is_empty then + errors.add new Error("Serialization Error: JSON `char` object does not declare a single char in `__val`.") + return object + end return val.chars.first end - print "Malformed Json string: unexpected Json Object kind '{kind or else "null"}'" - abort + errors.add new Error("Serialization Error: JSON object has an unknown `__kind`.") + return object end + # Simple JSON array without serialization metadata if object isa Array[nullable Object] then - # special case, isa Array[nullable Serializable] - var array = new Array[nullable Serializable] - for e in object do array.add e.as(nullable Serializable) + var array = new Array[nullable Object] + var types = new HashSet[String] + var has_nullable = false + for e in object do + var res = convert_object(e) + array.add res + + if res != null then + types.add res.class_name + else has_nullable = true + end + + if types.length == 1 then + var array_type = types.first + + var typed_array + if array_type == "ASCIIFlatString" or array_type == "UnicodeFlatString" then + if has_nullable then + typed_array = new Array[nullable FlatString] + else typed_array = new Array[FlatString] + else if array_type == "Int" then + if has_nullable then + typed_array = new Array[nullable Int] + else typed_array = new Array[Int] + else if array_type == "Float" then + if has_nullable then + typed_array = new Array[nullable Float] + else typed_array = new Array[Float] + else + # TODO support all array types when we separate the constructor + # `from_deserializer` from the filling of the items. + + if not has_nullable then + typed_array = new Array[Object] + else + # Unsupported array type, return as `Array[nullable Object]` + return array + end + end + + assert typed_array isa Array[nullable Object] + + # Copy item to the new array + for e in array do typed_array.add e + return typed_array + end + + # Uninferable type, return as `Array[nullable Object]` return array end return object end - redef fun deserialize do return convert_object(root) + redef fun deserialize + do + errors.clear + return convert_object(root) + end + + # User customizable heuristic to get the name of the Nit class to deserialize `json_object` + # + # This method is called only when deserializing an object without the metadata `__class`. + # Return the class name as a `String` when it can be inferred. + # Return `null` when the class name cannot be found. + # + # If a valid class name is returned, `json_object` will then be deserialized normally. + # So it must contain the attributes of the corresponding class, as usual. + # + # ~~~nitish + # class MyData + # serialize + # + # var data: String + # end + # + # class MyError + # serialize + # + # var error: String + # end + # + # class MyJsonDeserializer + # super JsonDeserializer + # + # redef fun class_name_heuristic(json_object) + # do + # if json_object.keys.has("error") then return "MyError" + # if json_object.keys.has("data") then return "MyData" + # return null + # end + # end + # + # var json = """{"data": "some other data"}""" + # var deserializer = new MyJsonDeserializer(json) + # var deserialized = deserializer.deserialize + # assert deserialized isa MyData + # + # json = """{"error": "some error message"}""" + # deserializer = new MyJsonDeserializer(json) + # deserialized = deserializer.deserialize + # assert deserialized isa MyError + # ~~~ + protected fun class_name_heuristic(json_object: Map[String, nullable Object]): nullable String + do + return null + end +end + +redef class Text + + # Deserialize a `nullable Object` from this JSON formatted string + # + # Warning: Deserialization errors are reported with `print_error` and + # may be returned as a partial object or as `null`. + # + # This method is not appropriate when errors need to be handled programmatically, + # manually use a `JsonDeserializer` in such cases. + fun from_json_string: nullable Object + do + var deserializer = new JsonDeserializer(self) + var res = deserializer.deserialize + if deserializer.errors.not_empty then + print_error "Deserialization Errors: {deserializer.errors.join(", ")}" + end + return res + end + + redef fun accept_json_serializer(v) do v.stream.write(to_json) end redef class Serializable - private fun serialize_to_json(v: JsonSerializer) + + # Serialize `self` to JSON + # + # Set `plain = true` to generate standard JSON, without deserialization metadata. + # Use this option if the generated JSON will be read by other programs or humans. + # Use the default, `plain = false`, if the JSON is to be deserialized by a Nit program. + # + # Set `pretty = true` to generate pretty JSON for human eyes. + # Use the default, `pretty = false`, to generate minified JSON. + # + # This method should not be refined by subclasses, + # instead `accept_json_serializer` can customize the serialization of an object. + # + # See: `JsonSerializer` + fun serialize_to_json(plain, pretty: nullable Bool): String + do + var stream = new StringWriter + var serializer = new JsonSerializer(stream) + serializer.plain_json = plain or else false + serializer.pretty_json = pretty or else false + serializer.serialize self + stream.close + return stream.to_s + end + + # Refinable service to customize the serialization of this class to JSON + # + # This method can be refined to customize the serialization by either + # writing pure JSON directly on the stream `v.stream` or + # by using other services of `JsonSerializer`. + # + # Most of the time, it is preferable to refine the method `core_serialize_to` + # which is used by all the serialization engines, not just JSON. + protected fun accept_json_serializer(v: JsonSerializer) do - var id = v.ref_id_for(self) + var id = v.cache.new_id_for(self) v.stream.write "\{" + v.indent_level += 1 if not v.plain_json then + v.new_line_and_indent v.stream.write "\"__kind\": \"obj\", \"__id\": " v.stream.write id.to_s v.stream.write ", \"__class\": \"" @@ -301,55 +560,40 @@ redef class Serializable v.stream.write "\"" end core_serialize_to(v) - v.stream.write "\}" - end - # Serialize this object to plain JSON - # - # This is a shortcut using `JsonSerializer::plain_json`, - # see its documentation for more information. - fun to_plain_json: String - do - var stream = new StringWriter - var serializer = new JsonSerializer(stream) - serializer.plain_json = true - serializer.serialize self - stream.close - return stream.to_s + v.indent_level -= 1 + v.new_line_and_indent + v.stream.write "\}" end end redef class Int - redef fun serialize_to_json(v) do v.stream.write(to_s) + redef fun accept_json_serializer(v) do v.stream.write to_s end redef class Float - redef fun serialize_to_json(v) do v.stream.write(to_s) + redef fun accept_json_serializer(v) do v.stream.write to_s end redef class Bool - redef fun serialize_to_json(v) do v.stream.write(to_s) + redef fun accept_json_serializer(v) do v.stream.write to_s end redef class Char - redef fun serialize_to_json(v) + redef fun accept_json_serializer(v) do if v.plain_json then - v.stream.write to_s.to_json + to_s.accept_json_serializer v else v.stream.write "\{\"__kind\": \"char\", \"__val\": " - v.stream.write to_s.to_json + to_s.accept_json_serializer v v.stream.write "\}" end end end -redef class String - redef fun serialize_to_json(v) do v.stream.write(to_json) -end - redef class NativeString - redef fun serialize_to_json(v) do to_s.serialize_to_json(v) + redef fun accept_json_serializer(v) do to_s.accept_json_serializer(v) end redef class Collection[E] @@ -357,115 +601,124 @@ redef class Collection[E] private fun serialize_to_pure_json(v: JsonSerializer) do v.stream.write "[" + v.indent_level += 1 var is_first = true for e in self do if is_first then is_first = false - else v.stream.write ", " + else v.stream.write "," + v.new_line_and_indent if not v.try_to_serialize(e) then + assert e != null # null would have been serialized v.warn("element of type {e.class_name} is not serializable.") end end + v.indent_level -= 1 + v.new_line_and_indent v.stream.write "]" end end redef class SimpleCollection[E] - redef fun serialize_to_json(v) + redef fun accept_json_serializer(v) do # Register as pseudo object if not v.plain_json then - var id = v.ref_id_for(self) - v.stream.write """{"__kind": "obj", "__id": """ + var id = v.cache.new_id_for(self) + v.stream.write """{""" + v.indent_level += 1 + v.new_line_and_indent + v.stream.write """"__kind": "obj", "__id": """ v.stream.write id.to_s v.stream.write """, "__class": """" v.stream.write class_name - v.stream.write """", "__length": """ - v.stream.write length.to_s - v.stream.write """, "__items": """ + v.stream.write """",""" + v.new_line_and_indent + v.stream.write """"__items": """ end serialize_to_pure_json v if not v.plain_json then + v.indent_level -= 1 + v.new_line_and_indent v.stream.write "\}" end end - redef init from_deserializer(v: Deserializer) + redef init from_deserializer(v) do super if v isa JsonDeserializer then v.notify_of_creation self init - var length = v.deserialize_attribute("__length").as(Int) var arr = v.path.last["__items"].as(SequenceRead[nullable Object]) - for i in length.times do - var obj = v.convert_object(arr[i]) + for o in arr do + var obj = v.convert_object(o) self.add obj end end end end -redef class Array[E] - redef fun serialize_to_json(v) - do - if v.plain_json or class_name == "Array[nullable Serializable]" then - # Using class_name to get the exact type, - # we do not want Array[Int] or anything else here. - - serialize_to_pure_json v - else super - end -end - redef class Map[K, V] - redef fun serialize_to_json(v) + redef fun accept_json_serializer(v) do # Register as pseudo object - var id = v.ref_id_for(self) + var id = v.cache.new_id_for(self) if v.plain_json then v.stream.write "\{" + v.indent_level += 1 var first = true for key, val in self do if not first then - v.stream.write ", " + v.stream.write "," else first = false + v.new_line_and_indent - if key == null then key = "null" - - v.stream.write key.to_s.to_json + var k = key or else "null" + k.to_s.accept_json_serializer v v.stream.write ": " if not v.try_to_serialize(val) then + assert val != null # null would have been serialized v.warn("element of type {val.class_name} is not serializable.") v.stream.write "null" end end + v.indent_level -= 1 + v.new_line_and_indent v.stream.write "\}" else - v.stream.write """{"__kind": "obj", "__id": """ + v.stream.write "\{" + v.indent_level += 1 + v.new_line_and_indent + v.stream.write """"__kind": "obj", "__id": """ v.stream.write id.to_s v.stream.write """, "__class": """" v.stream.write class_name v.stream.write """", "__length": """ v.stream.write length.to_s - v.stream.write """, "__keys": """ + v.stream.write "," + v.new_line_and_indent + v.stream.write """"__keys": """ keys.serialize_to_pure_json v - v.stream.write """, "__values": """ + v.stream.write "," + v.new_line_and_indent + v.stream.write """"__values": """ values.serialize_to_pure_json v + v.indent_level -= 1 + v.new_line_and_indent v.stream.write "\}" end end - # Instantiate a new `Array` from its serialized representation. - redef init from_deserializer(v: Deserializer) + redef init from_deserializer(v) do super