Skip to content

Commit

Permalink
Showing 5 changed files with 104 additions and 1 deletion.
40 changes: 40 additions & 0 deletions spec/std/json/mapping_spec.cr
Original file line number Diff line number Diff line change
@@ -118,6 +118,24 @@ class JSONWithRaw
})
end

class JSONWithRoot
JSON.mapping({
result: {type: Array(JSONPerson), root: "heroes"},
})
end

class JSONWithNilableRoot
JSON.mapping({
result: {type: Array(JSONPerson), root: "heroes", nilable: true},
})
end

class JSONWithNilableRootEmitNull
JSON.mapping({
result: {type: Array(JSONPerson), root: "heroes", nilable: true, emit_null: true},
})
end

describe "JSON mapping" do
it "parses person" do
person = JSONPerson.from_json(%({"name": "John", "age": 30}))
@@ -349,4 +367,26 @@ describe "JSON mapping" do
json.value.should eq(%([null,true,false,{"x":[1,1.5]}]))
json.to_json.should eq(string)
end

it "parses with root" do
json = %({"result":{"heroes":[{"name":"Batman"}]}})
result = JSONWithRoot.from_json(json)
result.result.should be_a(Array(JSONPerson))
result.result.first.name.should eq "Batman"
result.to_json.should eq(json)
end

it "parses with nilable root" do
json = %({"result":null})
result = JSONWithNilableRoot.from_json(json)
result.result.should be_nil
result.to_json.should eq("{}")
end

it "parses with nilable root and emit null" do
json = %({"result":null})
result = JSONWithNilableRootEmitNull.from_json(json)
result.result.should be_nil
result.to_json.should eq(json)
end
end
5 changes: 5 additions & 0 deletions spec/std/json/serialization_spec.cr
Original file line number Diff line number Diff line change
@@ -106,6 +106,11 @@ describe "JSON serialization" do
JSONSpecEnum.from_json(%("Three"))
end
end

it "deserializes with root" do
Int32.from_json(%({"foo": 1}), root: "foo").should eq(1)
Array(Int32).from_json(%({"foo": [1, 2]}), root: "foo").should eq([1, 2])
end
end

describe "to_json" do
25 changes: 25 additions & 0 deletions src/json/from_json.cr
Original file line number Diff line number Diff line change
@@ -1,8 +1,33 @@
# Deserializes the given JSON in *string_or_io* into
# an instance of `self`. This simply creates a `parser = JSON::PullParser`
# and invokes `new(parser)`: classes that want to provide JSON
# deserialization must provide an `def initialize(parser : JSON::PullParser`
# method.
#
# ```
# Int32.from_json("1") # => 1
# Array(Int32).from_json("[1, 2, 3]") # => [1, 2, 3]
# ```
def Object.from_json(string_or_io) : self
parser = JSON::PullParser.new(string_or_io)
new parser
end

# Deserializes the given JSON in *string_or_io* into
# an instance of `self`, assuming the JSON consists
# of an JSON object with key *root*, and whose value is
# the value to deserialize.
#
# ```
# Int32.from_json(%({"main": 1}), root: "main").should eq(1)
# ```
def Object.from_json(string_or_io, root : String) : self
parser = JSON::PullParser.new(string_or_io)
parser.on_key!(root) do
new parser
end
end

# Parses a String or IO denoting a JSON array, yielding
# each of its elements to the given block. This is useful
# for decoding an array and processing its elements without
30 changes: 30 additions & 0 deletions src/json/mapping.cr
Original file line number Diff line number Diff line change
@@ -41,6 +41,7 @@ module JSON
# * **default**: value to use if the property is missing in the JSON document, or if it's `null` and `nilable` was not set to `true`. If the default value creates a new instance of an object (for example `[1, 2, 3]` or `SomeObject.new`), a different instance will be used each time a JSON document is parsed.
# * **emit_null**: if true, emits a `null` value for nilable properties (by default nulls are not emitted)
# * **converter**: specify an alternate type for parsing and generation. The converter must define `from_json(JSON::PullParser)` and `to_json(value, IO)` as class methods. Examples of converters are `Time::Format` and `Time::EpochConverter` for `Time`.
# * **root**: assume the value is inside a JSON object with a given key (see `Object.from_json(string_or_io, root)`)
#
# The mapping also automatically defines Crystal properties (getters and setters) for each
# of the keys. It doesn't define a constructor accepting those arguments, but you can provide
@@ -84,16 +85,26 @@ module JSON
{% for key, value in properties %}
when {{value[:key] || key.id.stringify}}
%found{key.id} = true

%var{key.id} =
{% if value[:nilable] || value[:default] != nil %} %pull.read_null_or { {% end %}

{% if value[:root] %}
%pull.on_key!({{value[:root]}}) do
{% end %}

{% if value[:converter] %}
{{value[:converter]}}.from_json(%pull)
{% else %}
{{value[:type]}}.new(%pull)
{% end %}

{% if value[:root] %}
end
{% end %}

{% if value[:nilable] || value[:default] != nil %} } {% end %}

{% end %}
else
{% if strict %}
@@ -137,6 +148,17 @@ module JSON
{% end %}

json.field({{value[:key] || key.id.stringify}}) do
{% if value[:root] %}
{% if value[:emit_null] %}
if _{{key.id}}.is_a?(Nil)
nil.to_json(io)
else
{% end %}

io.json_object do |json|
json.field({{value[:root]}}) do
{% end %}

{% if value[:converter] %}
if _{{key.id}}
{{ value[:converter] }}.to_json(_{{key.id}}, io)
@@ -146,6 +168,14 @@ module JSON
{% else %}
_{{key.id}}.to_json(io)
{% end %}

{% if value[:root] %}
{% if value[:emit_null] %}
end
{% end %}
end
end
{% end %}
end

{% unless value[:emit_null] %}
5 changes: 4 additions & 1 deletion src/json/pull_parser.cr
Original file line number Diff line number Diff line change
@@ -226,11 +226,12 @@ class JSON::PullParser

def on_key!(key)
found = false
value = uninitialized typeof(yield)

read_object do |some_key|
if some_key == key
found = true
yield
value = yield
else
skip
end
@@ -239,6 +240,8 @@ class JSON::PullParser
unless found
raise "json key not found: #{key}"
end

value
end

def read_next

0 comments on commit fb22a89

Please sign in to comment.