# 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
#
# end
#
# var bob = new Person("Bob", 1986)
+# assert bob.serialize_to_json(pretty=true, plain=true) == """
+#{
+# "name": "Bob",
+# "year_of_birth": 1986,
+# "next_of_kin": null
+#}"""
+#
# var alice = new Person("Alice", 1978, bob)
+# 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
+# }
+#}"""
+#
+# # 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
+# }
+# }
+#}"""
+# ~~~
+#
+# ## 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:
+#
+# 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 Triangle
+# serialize
+#
+# var corners = new Array[Point]
+# redef var to_s is serialize_as("name")
+# end
+#
+# class Point
+# serialize
+#
+# var x: Int
+# var y: Int
+# end
+#
+# # 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"
+# }"""
+#
+# var deserializer = new JsonDeserializer(json_with_metadata)
+# var object = deserializer.deserialize
+# assert deserializer.errors.is_empty
+# assert object != null
+# print object
#
-# assert bob.to_plain_json == """
-# {"name": "Bob", "year_of_birth": 1986, "next_of_kin": null}"""
+# # 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"
+# }"""
#
-# assert alice.to_plain_json == """
-# {"name": "Alice", "year_of_birth": 1978, "next_of_kin": {"name": "Bob", "year_of_birth": 1986, "next_of_kin": null}}"""
+# 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
import ::serialization::caching
private import ::serialization::engine_tools
private import static
+private import string_parser
# Serializer of Nit objects to Json string.
class JsonSerializer
# 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:
#
# * 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.
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
end
first_attribute = true
- object.serialize_to_json self
+ object.accept_json_serializer self
first_attribute = false
if plain_json then open_objects.pop
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 "\": "
if not plain_json and cache.has_object(object) then
# if already serialized, add local reference
var id = cache.id_for(object)
- stream.write "\{\"__kind\": \"ref\", \"__id\": "
+ 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
serialize object
end
end
+
+ # Write a new line and indent it, only if `pretty_json`
+ private fun new_line_and_indent
+ do
+ 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.
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]
+ 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.
#
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)
+ 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
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
return null
end
- if object isa JsonObject then
+ if object isa Map[String, nullable Object] then
var kind = null
if object.keys.has("__kind") then
kind = object["__kind"]
end
end
- if not object.keys.has("__class") then
+ var class_name = object.get_or_null("__class")
+ 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
errors.add new Error("Serialization Error: JSON object declaration does not declare a `__class`.")
return object
end
- var class_name = object["__class"]
if not class_name isa String then
errors.add new Error("Serialization Error: JSON object declaration declares a non-string `__class`.")
return object
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)
+ # 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
+ 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
+
+ # Uninferrable type, return as `Array[nullable Object]`
return array
end
return object
end
- redef fun deserialize do return convert_object(root)
+ # 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 infer the name of the Nit class to deserialize `json_object`
+ #
+ # This method is called only when deserializing an object without the metadata `__class`.
+ # 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.
+ #
+ # ~~~nitish
+ # class MyData
+ # serialize
+ #
+ # var data: String
+ # end
+ #
+ # class MyError
+ # serialize
+ #
+ # var error: String
+ # var related_data: MyData
+ # end
+ #
+ # class MyJsonDeserializer
+ # super 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 data"}"""
+ # var deserializer = new MyJsonDeserializer(json)
+ # var deserialized = deserializer.deserialize
+ # assert deserialized isa MyData
+ #
+ # json = """{"error": "some error message",
+ # "related_data": {"data": "some other data"}"""
+ # 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 "\""
+ 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
- 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
+
+ # 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
+ # 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.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\": \""
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]
# Utility to serialize a normal Json array
private fun serialize_to_pure_json(v: JsonSerializer)
do
- v.stream.write "["
- var is_first = true
- for e in self do
- if is_first then
- is_first = false
- else v.stream.write ", "
-
- if not v.try_to_serialize(e) then
- v.warn("element of type {e.class_name} is not serializable.")
- end
+ 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
- v.stream.write "]"
+ 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.cache.new_id_for(self)
- 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 """, "__items": """
+ v.stream.write """","""
+ v.new_line_and_indent
+ v.stream.write """"__items": """
+
+ core_serialize_to v
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])
- 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
- 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.
+ # 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
- serialize_to_pure_json v
- else super
+ # 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
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.cache.new_id_for(self)
+ v.stream.write "\{"
+ v.indent_level += 1
+
if v.plain_json then
- v.stream.write "\{"
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.stream.write "\}"
else
- v.stream.write """{"__kind": "obj", "__id": """
+ 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.stream.write "\}"
+ core_serialize_to v
end
+
+ v.indent_level -= 1
+ v.new_line_and_indent
+ v.stream.write "\}"
end
- # Instantiate a new `Array` from its serialized representation.
- redef init from_deserializer(v: Deserializer)
+ redef init from_deserializer(v)
do
super
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