From b450c854714f5e5654a8b537fa9abbdc6abe8ff8 Mon Sep 17 00:00:00 2001
From: Konstantin Makarchev <kostya27@gmail.com>
Date: Wed, 25 Apr 2018 18:32:29 +0300
Subject: [PATCH 1/3] add json mapping extra fields

---
 spec/std/json/mapping_spec.cr | 15 +++++++++++++++
 src/json/mapping.cr           | 19 ++++++++++++++++++-
 2 files changed, 33 insertions(+), 1 deletion(-)

diff --git a/spec/std/json/mapping_spec.cr b/spec/std/json/mapping_spec.cr
index 10f7c5b0a56b..b88f65af10fe 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,14 @@ 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 "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..160a08c3a677 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 %}

From b4dc3caf527bdaf6851a2a676608f2e99476fa2f Mon Sep 17 00:00:00 2001
From: Konstantin Makarchev <kostya27@gmail.com>
Date: Wed, 25 Apr 2018 19:12:24 +0300
Subject: [PATCH 2/3] add extra for to_json also

---
 spec/std/json/mapping_spec.cr | 7 +++++++
 src/json/mapping.cr           | 6 ++++++
 2 files changed, 13 insertions(+)

diff --git a/spec/std/json/mapping_spec.cr b/spec/std/json/mapping_spec.cr
index b88f65af10fe..088c9771b953 100644
--- a/spec/std/json/mapping_spec.cr
+++ b/spec/std/json/mapping_spec.cr
@@ -293,6 +293,13 @@ describe "JSON mapping" do
     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 160a08c3a677..7e6b951a8a5f 100644
--- a/src/json/mapping.cr
+++ b/src/json/mapping.cr
@@ -238,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

From d167ca11175878f33ac7594b1fee530139b09e79 Mon Sep 17 00:00:00 2001
From: Konstantin Makarchev <kostya27@gmail.com>
Date: Wed, 25 Apr 2018 19:29:34 +0300
Subject: [PATCH 3/3] fix json

---
 spec/std/json/mapping_spec.cr | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/spec/std/json/mapping_spec.cr b/spec/std/json/mapping_spec.cr
index 088c9771b953..fa9319932f07 100644
--- a/spec/std/json/mapping_spec.cr
+++ b/spec/std/json/mapping_spec.cr
@@ -286,7 +286,7 @@ describe "JSON mapping" do
   end
 
   it "should unpack extra fields" do
-    person = JSONPersonWithExtra.from_json(%({"name": "John", "age": 30, "extra1" : 1, "extra2" : [1,2,3]}))
+    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
@@ -294,7 +294,7 @@ describe "JSON mapping" do
   end
 
   it "should pack extra fields" do
-    person = JSONPersonWithExtra.from_json(%({"name": "John", "age": 30, "extra1" : 1, "extra2" : [1,2,3]}))
+    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\"}"