# assert alice.to_plain_json == """
# {"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 meta-data
+# 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
+# assert meet isa MeetupConfig
+# assert meet.description == "My Awesome Meetup"
+# assert meet.max_participants == null
+# assert meet.answers == ["Pepperoni", "Chicken"]
+# ~~~
module serialization
import ::serialization::caching
# 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:
#
# * Does not support cycles, will replace the problematic references by `null`.
# * Does not serialize the meta-data 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
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]]
# 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
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)
+ 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
var array_type = types.first
var typed_array
- if array_type == "FlatString" then
+ 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]
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 serialize_to_json(v) do v.stream.write(to_json)
end
redef class Serializable
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.indent_level -= 1
+ v.new_line_and_indent
v.stream.write "\}"
end
+ # Serialize this object to a JSON string with metadata for deserialization
+ fun to_json_string: String
+ do
+ var stream = new StringWriter
+ var serializer = new JsonSerializer(stream)
+ serializer.serialize self
+ stream.close
+ return stream.to_s
+ end
+
# Serialize this object to plain JSON
#
# This is a shortcut using `JsonSerializer::plain_json`,
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)
end
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
# 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 """", "__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
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"
+ v.stream.write k.to_s.to_json
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