diff --git a/spec/std/json/mapping_spec.cr b/spec/std/json/mapping_spec.cr index 10f7c5b0a56b..fa9319932f07 100644 --- a/spec/std/json/mapping_spec.cr +++ b/spec/std/json/mapping_spec.cr @@ -30,6 +30,13 @@ private class JSONPersonEmittingNull }) end +private class JSONPersonWithExtra + JSON.mapping({ + name: {type: String}, + age: {type: Int32, nilable: true}, + }, extra: "other") +end + private class JSONWithBool JSON.mapping value: Bool end @@ -278,6 +285,21 @@ describe "JSON mapping" do ex.location.should eq({3, 15}) end + it "should unpack extra fields" do + person = JSONPersonWithExtra.from_json(%({"name": "John", "age": 30, "extra1": 1, "extra2": [1,2,3]})) + person.name.should eq("John") + person.age.should eq(30) + person.other["extra1"].should eq 1 + person.other["extra2"].should eq [1, 2, 3] + end + + it "should pack extra fields" do + person = JSONPersonWithExtra.from_json(%({"name": "John", "age": 30, "extra1": 1, "extra2": [1,2,3]})) + person.other["extra3"] = JSON::Any.new("bla") + person.other.delete("extra1") + person.to_json.should eq "{\"name\":\"John\",\"age\":30,\"extra2\":[1,2,3],\"extra3\":\"bla\"}" + end + it "doesn't emit null by default when doing to_json" do person = JSONPerson.from_json(%({"name": "John"})) (person.to_json =~ /age/).should be_falsey diff --git a/src/json/mapping.cr b/src/json/mapping.cr index 5fc113e57077..7e6b951a8a5f 100644 --- a/src/json/mapping.cr +++ b/src/json/mapping.cr @@ -65,7 +65,14 @@ module JSON # If *strict* is `true`, unknown properties in the JSON # document will raise a parse exception. The default is `false`, so unknown properties # are silently ignored. - macro mapping(_properties_, strict = false) + # + # If *extra* is a String, unknown properties in the JSON + # document will be stored into field with this name. + macro mapping(_properties_, strict = false, extra = nil) + {% if extra && _properties_.keys.includes? extra.id %} + {{ raise "Name for extra property already in use: #{extra.id}" }} + {% end %} + {% for key, value in _properties_ %} {% _properties_[key] = {type: value} unless value.is_a?(HashLiteral) || value.is_a?(NamedTupleLiteral) %} {% end %} @@ -98,6 +105,14 @@ module JSON {% end %} {% end %} + {% if extra %} + @{{extra.id}} = Hash(String, ::JSON::Any).new + + def {{extra.id}} + @{{extra.id}} + end + {% end %} + def initialize(%pull : ::JSON::PullParser) {% for key, value in _properties_ %} %var{key.id} = nil @@ -145,6 +160,8 @@ module JSON else {% if strict %} raise ::JSON::MappingError.new("Unknown JSON attribute: #{key}", self.class, *%key_location) + {% elsif extra %} + @{{extra.id}}[key] = ::JSON::Any.new(%pull) {% else %} %pull.skip {% end %} @@ -221,6 +238,12 @@ module JSON end {% end %} {% end %} + + {% if extra %} + @{{extra.id}}.each do |key, obj| + json.field(key) { obj.to_json(json) } + end + {% end %} end end end