Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

json::serialization: don't raise errors on missing attributes with default values #2302

Merged
merged 8 commits into from
Sep 1, 2016
85 changes: 84 additions & 1 deletion lib/json/serialization.nit
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,85 @@
# 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
Expand Down Expand Up @@ -295,13 +374,15 @@ class JsonDeserializer
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

Expand All @@ -310,6 +391,8 @@ class JsonDeserializer
attributes_path.add name
var res = convert_object(value, static_type)
attributes_path.pop

deserialize_attribute_missing = false
return res
end

Expand Down
6 changes: 6 additions & 0 deletions lib/serialization/serialization.nit
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,15 @@ abstract class Deserializer
# The `static_type` can be used as last resort if the deserialized object
# desn't have any metadata declaring the dynamic type.
#
# Return the deserialized value or null on error, and set
# `deserialize_attribute_missing` to whether the attribute was missing.
#
# Internal method to be implemented by the engines.
fun deserialize_attribute(name: String, static_type: nullable String): nullable Object is abstract

# Was the attribute queried by the last call to `deserialize_attribute` missing?
var deserialize_attribute_missing = false

# Register a newly allocated object (even if not completely built)
#
# Internal method called by objects in creation, to be implemented by the engines.
Expand Down
21 changes: 16 additions & 5 deletions src/frontend/serialization_phase.nit
Original file line number Diff line number Diff line change
Expand Up @@ -312,18 +312,29 @@ do
code.add """
self.{{{name}}} = v.deserialize_attribute("{{{attribute.serialize_name}}}", "{{{type_name}}}")
"""
else code.add """
else
code.add """
var {{{name}}} = v.deserialize_attribute("{{{attribute.serialize_name}}}", "{{{type_name}}}")
if not {{{name}}} isa {{{type_name}}} then
# Check if it was a subjectent error
v.errors.add new AttributeTypeError(self, "{{{attribute.serialize_name}}}", {{{name}}}, "{{{type_name}}}")
if v.deserialize_attribute_missing then
"""
# What to do when an attribute is missing?
if attribute.has_value then
# Leave it to the default value
else if mtype isa MNullableType then
code.add """
self.{{{name}}} = null"""
else code.add """
v.errors.add new Error("Deserialization Error: attribute `{class_name}::{{{name}}}` missing from JSON object")"""

# Clear subjacent error
code.add """
else if not {{{name}}} isa {{{type_name}}} then
v.errors.add new AttributeTypeError(self, "{{{attribute.serialize_name}}}", {{{name}}}, "{{{type_name}}}")
if v.keep_going == false then return
else
self.{{{name}}} = {{{name}}}
end
"""
end
end

code.add "end"
Expand Down
4 changes: 2 additions & 2 deletions src/modelize/modelize_property.nit
Original file line number Diff line number Diff line change
Expand Up @@ -1158,8 +1158,8 @@ redef class AAttrPropdef
# Is the node tagged optional?
var is_optional = false

# Has the node a default value?
# Could be through `n_expr` or `n_block`
# Does the node have a default value?
# Could be through `n_expr`, `n_block` or `is_lazy`
var has_value = false

# The guard associated to a lazy attribute.
Expand Down
2 changes: 1 addition & 1 deletion tests/sav/niti/test_json_deserialization_plain_alt2.res
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Runtime error: Uninitialized attribute _s (alt/test_json_deserialization_plain_alt2.nit:27)
# JSON: {"__class": "MyClass", "i": 123, "o": null}
# Errors: 'Deserialization Error: JSON object has not attribute 's'.', 'Deserialization Error: Wrong type on `MyClass::s` expected `String`, got `null`', 'Deserialization Error: JSON object has not attribute 'f'.', 'Deserialization Error: Wrong type on `MyClass::f` expected `Float`, got `null`', 'Deserialization Error: JSON object has not attribute 'a'.', 'Deserialization Error: Wrong type on `MyClass::a` expected `Array[String]`, got `null`'
# Errors: 'Deserialization Error: attribute `MyClass::s` missing from JSON object', 'Deserialization Error: attribute `MyClass::f` missing from JSON object', 'Deserialization Error: attribute `MyClass::a` missing from JSON object'
1 change: 0 additions & 1 deletion tests/sav/test_json_deserialization_plain.res
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
# Nit: <MyClass i:123 s:hello f:123.456 a:[one, two] o:<null>>

# JSON: {"__class": "MyClass", "i": 123, "s": "hello", "f": 123.456, "a": ["one", "two"]}
# Errors: 'Deserialization Error: JSON object has not attribute 'o'.'
# Nit: <MyClass i:123 s:hello f:123.456 a:[one, two] o:<null>>

# JSON: {"__class": "MyClass", "i": 123, "s": "hello", "f": 123.456, "a": ["one", "two"], "o":
Expand Down
2 changes: 1 addition & 1 deletion tests/sav/test_json_deserialization_plain_alt2.res
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Runtime error: Uninitialized attribute _s (alt/test_json_deserialization_plain_alt2.nit:22)
# JSON: {"__class": "MyClass", "i": 123, "o": null}
# Errors: 'Deserialization Error: JSON object has not attribute 's'.', 'Deserialization Error: Wrong type on `MyClass::s` expected `String`, got `null`', 'Deserialization Error: JSON object has not attribute 'f'.', 'Deserialization Error: Wrong type on `MyClass::f` expected `Float`, got `null`', 'Deserialization Error: JSON object has not attribute 'a'.', 'Deserialization Error: Wrong type on `MyClass::a` expected `Array[String]`, got `null`'
# Errors: 'Deserialization Error: attribute `MyClass::s` missing from JSON object', 'Deserialization Error: attribute `MyClass::f` missing from JSON object', 'Deserialization Error: attribute `MyClass::a` missing from JSON object'
2 changes: 1 addition & 1 deletion tests/test_json_deserialization_plain.nit
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ tests.add """
tests.add """
{"__class": "MyClass", "i": 123, "s": "hello", "f": 123.456, "o": null, "a": ["one", "two"], "Some random attribute": 777}"""

# Skipping `o` will cause an error but the attribute will be set to `null`
# Skipping `o` will set the attribute to `null`
tests.add """
{"__class": "MyClass", "i": 123, "s": "hello", "f": 123.456, "a": ["one", "two"]}"""

Expand Down