X-Git-Url: http://nitlanguage.org diff --git a/lib/json/serialization.nit b/lib/json/serialization.nit index 081c13b..d32b00e 100644 --- a/lib/json/serialization.nit +++ b/lib/json/serialization.nit @@ -47,8 +47,6 @@ # end # # var bob = new Person("Bob", 1986) -# var alice = new Person("Alice", 1978, bob) -# # assert bob.serialize_to_json(pretty=true, plain=true) == """ #{ # "name": "Bob", @@ -56,6 +54,7 @@ # "next_of_kin": null #}""" # +# var alice = new Person("Alice", 1978, bob) # assert alice.serialize_to_json(pretty=true, plain=true) == """ #{ # "name": "Alice", @@ -66,41 +65,172 @@ # "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 +# } +# } +#}""" # ~~~ # -# ## JSON to Nit objects +# ## Read JSON to create Nit objects +# +# The `JsonDeserializer` supports reading JSON code with or without metadata. +# It can create Nit objects from a remote service returning JSON data +# or to read local configuration files as Nit objects. +# However, it needs to know which Nit class to recreate from each JSON object. +# The class is either declared or inferred: # -# 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. +# 1. The JSON object defines a `__class` key with the name of the Nit class as value. +# This attribute is generated by the `JsonSerializer` with other metadata, +# it can also be specified by other external tools. +# 2. A refinement of `JsonDeserializer::class_name_heuristic` identifies the Nit class. +# 3. If all else fails, `JsonDeserializer` uses the static type of the attribute. # # ### Usage Example # # ~~~nitish # import json::serialization # -# class MeetupConfig +# class Triangle # serialize # -# var description: String -# var max_participants: nullable Int -# var answers: Array[FlatString] +# var corners = new Array[Point] +# redef var to_s is serialize_as("name") # end # -# var json_code = """ -# {"__class": "MeetupConfig", "description": "My Awesome Meetup", "max_participants": null, "answers": ["Pepperoni", "Chicken"]}""" -# var deserializer = new JsonDeserializer(json_code) +# class Point +# serialize +# +# var x: Int +# var y: Int +# end # -# var meet = deserializer.deserialize +# # 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" +# }""" # -# # Check for errors +# var deserializer = new JsonDeserializer(json_with_metadata) +# var object = deserializer.deserialize # assert deserializer.errors.is_empty +# assert object != null +# print object # -# assert meet isa MeetupConfig -# assert meet.description == "My Awesome Meetup" -# assert meet.max_participants == null -# assert meet.answers == ["Pepperoni", "Chicken"] +# # 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 calling +# # its deserialization constructor `from_deserializer`. +# 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 = new Triangle.from_deserializer(deserializer) +# assert deserializer.errors.is_empty # If false, `obj` is invalid +# print object +# ~~~ +# +# ### 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: +# +# 1. 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. +# 2. If the static type of the attribute is nullable, the engine sets +# the attribute to `null`. No error is raised. +# 3. 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. +# +# ~~~nitish +# import json::serialization +# +# 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 = new MyConfig.from_deserializer(deserializer) +# +# assert deserializer.errors.is_empty +# 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 = new MyConfig.from_deserializer(deserializer) +# +# assert deserializer.errors.is_empty +# 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 = new MyConfig.from_deserializer(deserializer) +# +# # There's an error, `obj` is partial +# assert deserializer.errors.length == 1 +# +# # Still, we can access valid attributes +# assert obj.player_name == "Bob" # ~~~ module serialization @@ -242,6 +372,9 @@ class JsonDeserializer # Depth-first path in the serialized object tree. private var path = new Array[Map[String, nullable Object]] + # Names of the attributes from the root to the object currently being deserialized + var attributes_path = new Array[String] + # Last encountered object reference id. # # See `id_to_object`. @@ -253,19 +386,33 @@ class JsonDeserializer self.root = root end - redef fun deserialize_attribute(name) + redef fun deserialize_attribute(name, static_type) do - assert not path.is_empty # This is an internal error, abort + if path.is_empty then + # The was a parsing error or the root is not an object + if not root isa Error then + errors.add new Error("Deserialization Error: parsed JSON value is not an object.") + end + deserialize_attribute_missing = false + return null + end + var current = path.last if not current.keys.has(name) then - errors.add new Error("Deserialization Error: JSON object has not attribute '{name}'.") + # Let the generated code / caller of `deserialize_attribute` raise the missing attribute error + deserialize_attribute_missing = true return null end var value = current[name] - return convert_object(value) + attributes_path.add name + var res = convert_object(value, static_type) + attributes_path.pop + + deserialize_attribute_missing = false + return res end # This may be called multiple times by the same object from constructors @@ -277,8 +424,8 @@ class JsonDeserializer cache[id] = new_object end - # Convert from simple Json object to Nit object - private fun convert_object(object: nullable Object): nullable Object + # Convert the simple JSON `object` to a Nit object + private fun convert_object(object: nullable Object, static_type: nullable String): nullable Object do if object isa JsonParseError then errors.add object @@ -333,6 +480,14 @@ class JsonDeserializer if class_name == null then # Fallback to custom heuristic class_name = class_name_heuristic(object) + + if class_name == null and static_type != null then + # Fallack to the static type, strip the `nullable` prefix + var prefix = "nullable " + if static_type.has(prefix) then + class_name = static_type.substring_from(prefix.length) + else class_name = static_type + end end if class_name == null then @@ -381,6 +536,23 @@ class JsonDeserializer # Simple JSON array without serialization metadata if object isa Array[nullable Object] then + # Can we use the static type? + if static_type != null then + var prefix = "nullable " + var class_name = if static_type.has(prefix) then + static_type.substring_from(prefix.length) + else static_type + + opened_array = object + var value = deserialize_class(class_name) + opened_array = null + return value + end + + # This branch should rarely be used: + # when an array is the root object which is accepted but illegal in standard JSON, + # or in strange custom deserialization hacks. + var array = new Array[nullable Object] var types = new HashSet[String] var has_nullable = false @@ -428,24 +600,32 @@ class JsonDeserializer return typed_array end - # Uninferable type, return as `Array[nullable Object]` + # Uninferrable type, return as `Array[nullable Object]` return array end return object end + # Current array open for deserialization, used by `SimpleCollection::from_deserializer` + private var opened_array: nullable Array[nullable Object] = null + 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` + # User customizable heuristic to infer 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. + # Use the content of `json_object` to identify what Nit class it should be deserialized into. + # Or use `self.attributes_path` indicating where the deserialized object will be stored, + # is is less reliable as some objects don't have an associated attribute: + # the root/first deserialized object and collection elements. + # + # Return the class name as a `String` when it can be inferred, + # or `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. @@ -461,6 +641,7 @@ class JsonDeserializer # serialize # # var error: String + # var related_data: MyData # end # # class MyJsonDeserializer @@ -468,18 +649,26 @@ class JsonDeserializer # # redef fun class_name_heuristic(json_object) # do + # # Infer the Nit class from the content of the JSON object. # if json_object.keys.has("error") then return "MyError" # if json_object.keys.has("data") then return "MyData" + # + # # Infer the Nit class from the attribute where it will be stored. + # # This line duplicates a previous line, and would only apply when + # # `MyData` is within a `MyError`. + # if attributes_path.not_empty and attributes_path.last == "related_data" then return "MyData" + # # return null # end # end # - # var json = """{"data": "some other data"}""" + # var json = """{"data": "some data"}""" # var deserializer = new MyJsonDeserializer(json) # var deserialized = deserializer.deserialize # assert deserialized isa MyData # - # json = """{"error": "some error message"}""" + # json = """{"error": "some error message", + # "related_data": {"data": "some other data"}""" # deserializer = new MyJsonDeserializer(json) # deserialized = deserializer.deserialize # assert deserialized isa MyError @@ -509,7 +698,31 @@ redef class Text return res end - redef fun accept_json_serializer(v) do v.stream.write(to_json) + redef fun accept_json_serializer(v) + do + v.stream.write "\"" + for i in [0 .. self.length[ do + var char = self[i] + if char == '\\' then + v.stream.write "\\\\" + else if char == '\"' then + v.stream.write "\\\"" + else if char < ' ' then + if char == '\n' then + v.stream.write "\\n" + else if char == '\r' then + v.stream.write "\\r" + else if char == '\t' then + v.stream.write "\\t" + else + v.stream.write char.escape_to_utf16 + end + else + v.stream.write char.to_s + end + end + v.stream.write "\"" + end end redef class Serializable @@ -538,6 +751,16 @@ redef class Serializable return stream.to_s end + # Serialize `self` to plain JSON + # + # Compatibility alias for `serialize_to_json(plain=true)`. + fun to_json: String do return serialize_to_json(plain=true) + + # Serialize `self` to plain pretty JSON + # + # Compatibility alias for `serialize_to_json(plain=true, pretty=true)`. + fun to_pretty_json: String do return serialize_to_json(plain=true, pretty=true) + # Refinable service to customize the serialization of this class to JSON # # This method can be refined to customize the serialization by either @@ -600,23 +823,23 @@ redef class Collection[E] # Utility to serialize a normal Json array 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 "," - v.new_line_and_indent + 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 "," + 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 + 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 - v.indent_level -= 1 - v.new_line_and_indent - v.stream.write "]" + end + v.indent_level -= 1 + v.new_line_and_indent + v.stream.write "]" end end @@ -656,10 +879,37 @@ redef class SimpleCollection[E] v.notify_of_creation self init - var arr = v.path.last["__items"].as(SequenceRead[nullable Object]) - for o in arr do - var obj = v.convert_object(o) - self.add obj + var open_array: nullable SequenceRead[nullable Object] = v.opened_array + if open_array == null then + # With metadata + var arr = v.path.last.get_or_null("__items") + if not arr isa SequenceRead[nullable Object] then + # If there is nothing, we consider that it is an empty collection. + if arr != null then v.errors.add new Error("Deserialization Error: invalid format in {self.class_name}") + return + end + open_array = arr + end + + # Try to get the name of the single parameter type assuming it is E. + # This does not work in non-generic subclasses, + # when the first parameter is not E, or + # when there is more than one parameter. (The last one could be fixed) + var class_name = class_name + var items_type = null + var bracket_index = class_name.index_of('[') + if bracket_index != -1 then + var start = bracket_index + 1 + var ending = class_name.last_index_of(']') + items_type = class_name.substring(start, ending-start) + end + + # Fill array + for o in open_array do + var obj = v.convert_object(o, items_type) + if obj isa E then + add obj + else v.errors.add new AttributeTypeError(self, "items", obj, "E") end end end @@ -726,12 +976,44 @@ redef class Map[K, V] v.notify_of_creation self init - var length = v.deserialize_attribute("__length").as(Int) - var keys = v.path.last["__keys"].as(SequenceRead[nullable Object]) - var values = v.path.last["__values"].as(SequenceRead[nullable Object]) + var length = v.deserialize_attribute("__length") + var keys = v.path.last.get_or_null("__keys") + var values = v.path.last.get_or_null("__values") + + # Length is optional + if length == null and keys isa SequenceRead[nullable Object] then length = keys.length + + # Consistency check + if not length isa Int or length < 0 or + not keys isa SequenceRead[nullable Object] or + not values isa SequenceRead[nullable Object] or + keys.length != values.length or length != keys.length then + + # If there is nothing or length == 0, we consider that it is an empty Map. + if (length != null and length != 0) or keys != null or values != null then + v.errors.add new Error("Deserialization Error: invalid format in {self.class_name}") + end + return + end + for i in length.times do var key = v.convert_object(keys[i]) var value = v.convert_object(values[i]) + + if not key isa K then + v.errors.add new AttributeTypeError(self, "keys", key, "K") + continue + end + + if not value isa V then + v.errors.add new AttributeTypeError(self, "values", value, "V") + continue + end + + if has_key(key) then + v.errors.add new Error("Deserialization Error: duplicated key '{key or else "null"}' in {self.class_name}, previous value overwritten") + end + self[key] = value end end