require "../spec_helper" require "json" require "yaml" require "big" require "big/json" require "uuid" require "uuid/json" enum JSONSerializableEnum Zero One Two OneHundred end @[Flags] enum JSONSerializableFlagEnum One Two OneHundred end class JSONAttrValue(T) include JSON::Serializable property value : T end record JSONAttrPoint, x : Int32, y : Int32 do include JSON::Serializable end class JSONAttrEmptyClass include JSON::Serializable def initialize; end end class JSONAttrEmptyClassWithUnmapped include JSON::Serializable include JSON::Serializable::Unmapped def initialize; end end class JSONAttrPerson include JSON::Serializable property name : String property age : Int32? def_equals name, age def initialize(@name : String) end end struct JSONAttrPersonWithTwoFieldInInitialize include JSON::Serializable property name : String property age : Int32 def initialize(@name, @age) end end class StrictJSONAttrPerson include JSON::Serializable include JSON::Serializable::Strict property name : String property age : Int32? end class JSONAttrPersonExtraFields include JSON::Serializable include JSON::Serializable::Unmapped property name : String property age : Int32? end class JSONAttrPersonEmittingNull include JSON::Serializable property name : String @[JSON::Field(emit_null: true)] property age : Int32? end @[JSON::Serializable::Options(emit_nulls: true)] class JSONAttrPersonEmittingNullsByOptions include JSON::Serializable property name : String property age : Int32? property value1 : Int32? @[JSON::Field(emit_null: false)] property value2 : Int32? end class JSONAttrWithTime include JSON::Serializable @[JSON::Field(converter: Time::Format.new("%F %T"))] property value : Time end class JSONAttrWithNilableTime include JSON::Serializable @[JSON::Field(converter: Time::Format.new("%F"))] property value : Time? def initialize end end class JSONAttrWithNilableTimeEmittingNull include JSON::Serializable @[JSON::Field(converter: Time::Format.new("%F"), emit_null: true)] property value : Time? def initialize end end class JSONAttrWithTimeArray1 include JSON::Serializable @[JSON::Field(converter: JSON::ArrayConverter(Time::EpochConverter))] property value : Array(Time) end class JSONAttrWithTimeArray2 include JSON::Serializable @[JSON::Field(converter: JSON::ArrayConverter.new(Time::EpochConverter))] property value : Array(Time) end class JSONAttrWithTimeArray3 include JSON::Serializable @[JSON::Field(converter: JSON::ArrayConverter.new(Time::Format.new("%F %T")))] property value : Array(Time) end class JSONAttrWithTimeHash1 include JSON::Serializable @[JSON::Field(converter: JSON::HashValueConverter(Time::EpochConverter))] property value : Hash(String, Time) end class JSONAttrWithTimeHash2 include JSON::Serializable @[JSON::Field(converter: JSON::HashValueConverter.new(Time::EpochConverter))] property value : Hash(String, Time) end class JSONAttrWithTimeHash3 include JSON::Serializable @[JSON::Field(converter: JSON::HashValueConverter.new(Time::Format.new("%F %T")))] property value : Hash(String, Time) end class JSONAttrWithSimpleMapping include JSON::Serializable property name : String property age : Int32 end class JSONAttrWithKeywordsMapping include JSON::Serializable property end : Int32 property abstract : Int32 end class JSONAttrWithAny include JSON::Serializable property name : String property any : JSON::Any end class JSONAttrWithProblematicKeys include JSON::Serializable property key : Int32 property pull : Int32 end class JSONAttrWithDefaults include JSON::Serializable property a = 11 property b = "Haha" property c = true property d = false property e : Bool? = false property f : Int32? = 1 property g : Int32? property h = [1, 2, 3] end class JSONAttrWithSmallIntegers include JSON::Serializable property foo : Int16 property bar : Int8 end class JSONAttrWithTimeEpoch include JSON::Serializable @[JSON::Field(converter: Time::EpochConverter)] property value : Time end class JSONAttrNilableWithTimeEpoch include JSON::Serializable @[JSON::Field(converter: Time::EpochConverter)] property value : Time? end class JSONAttrDefaultWithTimeEpoch include JSON::Serializable @[JSON::Field(converter: Time::EpochConverter)] property value : Time = Time.unix(0) end class JSONAttrWithTimeEpochMillis include JSON::Serializable @[JSON::Field(converter: Time::EpochMillisConverter)] property value : Time end class JSONAttrWithRaw include JSON::Serializable @[JSON::Field(converter: String::RawConverter)] property value : String end class JSONAttrWithRoot include JSON::Serializable @[JSON::Field(root: "heroes")] property result : Array(JSONAttrPerson) end class JSONAttrWithNilableRoot include JSON::Serializable @[JSON::Field(root: "heroes")] property result : Array(JSONAttrPerson)? end class JSONAttrWithNilableRootEmitNull include JSON::Serializable @[JSON::Field(root: "heroes", emit_null: true)] property result : Array(JSONAttrPerson)? end class JSONAttrWithPresence include JSON::Serializable @[JSON::Field(presence: true)] property first_name : String? @[JSON::Field(presence: true)] property last_name : String? @[JSON::Field(ignore: true)] getter? first_name_present : Bool @[JSON::Field(ignore: true)] getter? last_name_present : Bool end class JSONAttrWithPresenceAndIgnoreSerialize include JSON::Serializable @[JSON::Field(presence: true, ignore_serialize: ignore_first_name?)] property first_name : String? @[JSON::Field(presence: true, ignore_serialize: last_name.nil? && !last_name_present?, emit_null: true)] property last_name : String? @[JSON::Field(ignore: true)] getter? first_name_present : Bool = false @[JSON::Field(ignore: true)] getter? last_name_present : Bool = false def initialize(@first_name : String? = nil, @last_name : String? = nil) end def ignore_first_name? first_name.nil? || first_name == "" end end class JSONAttrWithQueryAttributes include JSON::Serializable property? foo : Bool @[JSON::Field(key: "is_bar", presence: true)] property? bar : Bool = false @[JSON::Field(ignore: true)] getter? bar_present : Bool end class JSONAttrWithKeyQueryAttribute include JSON::Serializable @[JSON::Field(key: "is_foo")] property? foo : Bool end module JSONAttrModule property moo : Int32 = 10 end class JSONAttrModuleTest include JSONAttrModule include JSON::Serializable @[JSON::Field(key: "phoo")] property foo = 15 def initialize; end def to_tuple {@moo, @foo} end end class JSONAttrModuleTest2 < JSONAttrModuleTest property bar : Int32 def initialize(@bar : Int32); end def to_tuple {@moo, @foo, @bar} end end struct JSONAttrPersonWithYAML include JSON::Serializable include YAML::Serializable property name : String property age : Int32? def initialize(@name : String, @age : Int32? = nil) end end struct JSONAttrPersonWithYAMLInitializeHook include JSON::Serializable include YAML::Serializable property name : String property age : Int32? def initialize(@name : String, @age : Int32? = nil) after_initialize end @[JSON::Field(ignore: true)] @[YAML::Field(ignore: true)] property msg : String? def after_initialize @msg = "Hello " + name end end struct JSONAttrPersonWithSelectiveSerialization include JSON::Serializable property name : String @[JSON::Field(ignore_serialize: true)] property password : String @[JSON::Field(ignore_deserialize: true)] property generated : String = "generated-internally" def initialize(@name : String, @password : String) end end struct JSONAttrWithGenericConverter(T) include JSON::Serializable @[JSON::Field(converter: T)] property value : Time end abstract class JSONShape include JSON::Serializable use_json_discriminator "type", {point: JSONPoint, circle: JSONCircle} property type : String end class JSONPoint < JSONShape property x : Int32 property y : Int32 end class JSONCircle < JSONShape property x : Int32 property y : Int32 property radius : Int32 end enum JSONVariableDiscriminatorEnumFoo Foo = 4 end enum JSONVariableDiscriminatorEnumFoo8 : UInt8 Foo = 1_8 end class JSONVariableDiscriminatorValueType include JSON::Serializable use_json_discriminator "type", { 0 => JSONVariableDiscriminatorNumber, "1" => JSONVariableDiscriminatorString, true => JSONVariableDiscriminatorBoolTrue, false => JSONVariableDiscriminatorBoolFalse, JSONVariableDiscriminatorEnumFoo::Foo => JSONVariableDiscriminatorEnum, JSONVariableDiscriminatorEnumFoo8::Foo => JSONVariableDiscriminatorEnum8, } end class JSONVariableDiscriminatorNumber < JSONVariableDiscriminatorValueType end class JSONVariableDiscriminatorString < JSONVariableDiscriminatorValueType end class JSONVariableDiscriminatorBoolTrue < JSONVariableDiscriminatorValueType end class JSONVariableDiscriminatorBoolFalse < JSONVariableDiscriminatorValueType end class JSONVariableDiscriminatorEnum < JSONVariableDiscriminatorValueType end class JSONVariableDiscriminatorEnum8 < JSONVariableDiscriminatorValueType end class JSONStrictDiscriminator include JSON::Serializable include JSON::Serializable::Strict property type : String use_json_discriminator "type", {foo: JSONStrictDiscriminatorFoo, bar: JSONStrictDiscriminatorBar} end class JSONStrictDiscriminatorFoo < JSONStrictDiscriminator end class JSONStrictDiscriminatorBar < JSONStrictDiscriminator property x : JSONStrictDiscriminator property y : JSONStrictDiscriminator end module JSONNamespace struct FooRequest include JSON::Serializable getter foo : Foo getter bar = Bar.new end struct Foo include JSON::Serializable getter id = "id:foo" end struct Bar include JSON::Serializable getter id = "id:bar" def initialize # Allow for default value above end end end class JSONSomething include JSON::Serializable property value : JSONSomething? end module JsonDiscriminatorBug abstract class Base include JSON::Serializable use_json_discriminator("type", {"a" => A, "b" => B, "c" => C}) end class A < Base end class B < Base property source : Base property value : Int32 = 1 end class C < B end end record JSONAttrWithEnumValue, value : JSONSerializableEnum do include JSON::Serializable @[JSON::Field(converter: Enum::ValueConverter(JSONSerializableEnum))] @value : JSONSerializableEnum end record JSONAttrWithFlagEnumValue, value : JSONSerializableFlagEnum do include JSON::Serializable @[JSON::Field(converter: Enum::ValueConverter(JSONSerializableFlagEnum))] @value : JSONSerializableFlagEnum end abstract class SerializableFoo include JSON::Serializable module Converter def self.from_json(pull : JSON::PullParser) : SerializableFoo SerializableFoo.find.from_json("{}") end end def self.find : SerializableFoo.class SerializableBar.as(SerializableFoo.class) end end class SerializableBar < SerializableFoo @[JSON::Field(converter: SerializableFoo::Converter)] getter foo : SerializableFoo = SerializableBaz.new end class SerializableBaz < SerializableFoo def initialize end end class JSONInitializeOpts include JSON::Serializable property value : Int32 def initialize(**opts) @value = opts.size end end record Namespaced::JSON::Wrapper, name : String, options : Hash(String, ::JSON::Any::Type)? = nil do include ::JSON::Serializable end describe "JSON::Serializable" do it "works with classes within `JSON` namespace" do Namespaced::JSON::Wrapper .from_json(<<-JSON) { "name": "foo", "options": { "foo": true } } JSON .to_json end it "works with record" do JSONAttrPoint.new(1, 2).to_json.should eq "{\"x\":1,\"y\":2}" JSONAttrPoint.from_json(%({"x": 1, "y": 2})).should eq JSONAttrPoint.new(1, 2) end it "empty class" do e = JSONAttrEmptyClass.new e.to_json.should eq "{}" JSONAttrEmptyClass.from_json("{}") end it "empty class with unmapped" do JSONAttrEmptyClassWithUnmapped.from_json(%({"name": "John", "age": 30})).json_unmapped.should eq({"name" => "John", "age" => 30}) end it "parses person" do person = JSONAttrPerson.from_json(%({"name": "John", "age": 30})) person.should be_a(JSONAttrPerson) person.name.should eq("John") person.age.should eq(30) end it "parses person without age" do person = JSONAttrPerson.from_json(%({"name": "John"})) person.should be_a(JSONAttrPerson) person.name.should eq("John") person.name.size.should eq(4) # This verifies that name is not nilable person.age.should be_nil end it "parses array of people" do people = Array(JSONAttrPerson).from_json(%([{"name": "John"}, {"name": "Doe"}])) people.size.should eq(2) end it "works with class with two fields" do person1 = JSONAttrPersonWithTwoFieldInInitialize.from_json(%({"name": "John", "age": 30})) person2 = JSONAttrPersonWithTwoFieldInInitialize.new("John", 30) person1.should eq person2 end it "does to_json" do person = JSONAttrPerson.from_json(%({"name": "John", "age": 30})) person2 = JSONAttrPerson.from_json(person.to_json) person2.should eq(person) end it "parses person with unknown attributes" do person = JSONAttrPerson.from_json(%({"name": "John", "age": 30, "foo": "bar"})) person.should be_a(JSONAttrPerson) person.name.should eq("John") person.age.should eq(30) end it "parses strict person with unknown attributes" do error_message = <<-'MSG' Unknown JSON attribute: foo parsing StrictJSONAttrPerson MSG ex = expect_raises ::JSON::SerializableError, error_message do StrictJSONAttrPerson.from_json <<-JSON { "name": "John", "age": 30, "foo": "bar" } JSON end ex.location.should eq({4, 3}) ex.attribute.should eq "foo" end it "should parse extra fields (JSONAttrPersonExtraFields with on_unknown_json_attribute)" do person = JSONAttrPersonExtraFields.from_json(%({"name": "John", "age": 30, "x": "1", "y": 2, "z": [1,2,3]})) person.name.should eq("John") person.age.should eq(30) person.json_unmapped.should eq({"x" => "1", "y" => 2_i64, "z" => [1, 2, 3]}) end it "should to store extra fields (JSONAttrPersonExtraFields with on_to_json)" do person = JSONAttrPersonExtraFields.from_json(%({"name": "John", "age": 30, "x": "1", "y": 2, "z": [1,2,3]})) person.name = "John1" person.json_unmapped.delete("y") person.json_unmapped["q"] = JSON::Any.new("w") person.to_json.should eq "{\"name\":\"John1\",\"age\":30,\"x\":\"1\",\"z\":[1,2,3],\"q\":\"w\"}" end it "raises if non-nilable attribute is nil" do error_message = <<-'MSG' Missing JSON attribute: name parsing JSONAttrPerson#name at line 1, column 1 MSG ex = expect_raises ::JSON::SerializableError, error_message do JSONAttrPerson.from_json(%({"age": 30})) end ex.location.should eq({1, 1}) ex.attribute.should eq "name" end it "raises if not an object" do error_message = <<-'MSG' Expected BeginObject but was String at line 1, column 1 parsing StrictJSONAttrPerson at line 0, column 0 MSG ex = expect_raises ::JSON::SerializableError, error_message do StrictJSONAttrPerson.from_json <<-JSON "foo" JSON end ex.location.should eq({1, 1}) end it "raises if data type does not match" do error_message = <<-MSG Couldn't parse (Int32 | Nil) from "foo" at line 3, column 10 MSG ex = expect_raises ::JSON::SerializableError, error_message do StrictJSONAttrPerson.from_json <<-JSON { "name": "John", "age": "foo", "foo": "bar" } JSON end ex.location.should eq({3, 10}) ex.attribute.should eq "age" end it "doesn't emit null by default when doing to_json" do person = JSONAttrPerson.from_json(%({"name": "John"})) person.to_json.should_not match /age/ end it "emits null on request when doing to_json" do person = JSONAttrPersonEmittingNull.from_json(%({"name": "John"})) person.to_json.should match /age/ end it "emit_nulls option" do person = JSONAttrPersonEmittingNullsByOptions.from_json(%({"name": "John"})) person.to_json.should eq "{\"name\":\"John\",\"age\":null,\"value1\":null}" end it "doesn't raises on false value when not-nil" do json = JSONAttrValue(Bool).from_json(%({"value": false})) json.value.should be_false end it "parses JSON integer into a float property (#8618)" do json = JSONAttrValue(Float64).from_json(%({"value": 123})) json.value.should eq(123.0) end it "parses UUID" do uuid = JSONAttrValue(UUID).from_json(%({"value": "ba714f86-cac6-42c7-8956-bcf5105e1b81"})) uuid.should be_a(JSONAttrValue(UUID)) uuid.value.should eq(UUID.new("ba714f86-cac6-42c7-8956-bcf5105e1b81")) end it "parses json with Time::Format converter" do json = JSONAttrWithTime.from_json(%({"value": "2014-10-31 23:37:16"})) json.value.should be_a(Time) json.value.to_s.should eq("2014-10-31 23:37:16 UTC") json.to_json.should eq(%({"value":"2014-10-31 23:37:16"})) end it "allows setting a nilable property to nil" do person = JSONAttrPerson.new("John") person.age = 1 person.age = nil end it "parses simple mapping" do person = JSONAttrWithSimpleMapping.from_json(%({"name": "John", "age": 30})) person.should be_a(JSONAttrWithSimpleMapping) person.name.should eq("John") person.age.should eq(30) end it "outputs with converter when nilable" do json = JSONAttrWithNilableTime.new json.to_json.should eq("{}") end it "outputs with converter when nilable when emit_null is true" do json = JSONAttrWithNilableTimeEmittingNull.new json.to_json.should eq(%({"value":null})) end it "outputs JSON with Hash" do input = { value: {"foo" => "bar"}, }.to_json json = JSONAttrValue(Hash(String, String)).from_json(input) json.to_json.should eq(input) end it "parses json with keywords" do json = JSONAttrWithKeywordsMapping.from_json(%({"end": 1, "abstract": 2})) json.end.should eq(1) json.abstract.should eq(2) end it "parses json with any" do json = JSONAttrWithAny.from_json(%({"name": "Hi", "any": [{"x": 1}, 2, "hey", true, false, 1.5, null]})) json.name.should eq("Hi") json.any.raw.should eq([{"x" => 1}, 2, "hey", true, false, 1.5, nil]) json.to_json.should eq(%({"name":"Hi","any":[{"x":1},2,"hey",true,false,1.5,null]})) end it "parses json with problematic keys" do json = JSONAttrWithProblematicKeys.from_json(%({"key": 1, "pull": 2})) json.key.should eq(1) json.pull.should eq(2) end it "parses json array as set" do json = JSONAttrValue(Set(String)).from_json(%({"value": ["a", "a", "b"]})) json.value.should eq(Set(String){"a", "b"}) end it "allows small types of integer" do json = JSONAttrWithSmallIntegers.from_json(%({"foo": 23, "bar": 7})) json.foo.should eq(23) typeof(json.foo).should eq(Int16) json.bar.should eq(7) typeof(json.bar).should eq(Int8) end describe "parses json with defaults" do it "mixed" do json = JSONAttrWithDefaults.from_json(%({"a":1,"b":"bla"})) json.a.should eq 1 json.b.should eq "bla" json = JSONAttrWithDefaults.from_json(%({"a":1})) json.a.should eq 1 json.b.should eq "Haha" json = JSONAttrWithDefaults.from_json(%({"b":"bla"})) json.a.should eq 11 json.b.should eq "bla" json = JSONAttrWithDefaults.from_json(%({})) json.a.should eq 11 json.b.should eq "Haha" json = JSONAttrWithDefaults.from_json(%({"a":null,"b":null,"f":null})) json.a.should eq 11 json.b.should eq "Haha" json.f.should be_nil end it "bool" do json = JSONAttrWithDefaults.from_json(%({})) json.c.should be_true typeof(json.c).should eq Bool json.d.should be_false typeof(json.d).should eq Bool json = JSONAttrWithDefaults.from_json(%({"c":false})) json.c.should be_false json = JSONAttrWithDefaults.from_json(%({"c":true})) json.c.should be_true json = JSONAttrWithDefaults.from_json(%({"d":false})) json.d.should be_false json = JSONAttrWithDefaults.from_json(%({"d":true})) json.d.should be_true end it "with nilable" do json = JSONAttrWithDefaults.from_json(%({})) json.e.should be_false typeof(json.e).should eq(Bool | Nil) json.f.should eq 1 typeof(json.f).should eq(Int32 | Nil) json.g.should be_nil typeof(json.g).should eq(Int32 | Nil) json = JSONAttrWithDefaults.from_json(%({"e":false})) json.e.should be_false json = JSONAttrWithDefaults.from_json(%({"e":true})) json.e.should be_true end it "create new array every time" do json = JSONAttrWithDefaults.from_json(%({})) json.h.should eq [1, 2, 3] json.h << 4 json.h.should eq [1, 2, 3, 4] json = JSONAttrWithDefaults.from_json(%({})) json.h.should eq [1, 2, 3] end end it "converter with null value (#13655)" do JSONAttrNilableWithTimeEpoch.from_json(%({"value": null})).value.should be_nil JSONAttrNilableWithTimeEpoch.from_json(%({"value":1459859781})).value.should eq Time.unix(1459859781) end it "converter with default value" do JSONAttrDefaultWithTimeEpoch.from_json(%({"value": null})).value.should eq Time.unix(0) JSONAttrDefaultWithTimeEpoch.from_json(%({"value":1459859781})).value.should eq Time.unix(1459859781) end it "uses Time::EpochConverter" do string = %({"value":1459859781}) json = JSONAttrWithTimeEpoch.from_json(string) json.value.should be_a(Time) json.value.should eq(Time.unix(1459859781)) json.to_json.should eq(string) end it "uses Time::EpochMillisConverter" do string = %({"value":1459860483856}) json = JSONAttrWithTimeEpochMillis.from_json(string) json.value.should be_a(Time) json.value.should eq(Time.unix_ms(1459860483856)) json.to_json.should eq(string) end describe JSON::ArrayConverter do it "uses converter metaclass" do string = %({"value":[1459859781]}) json = JSONAttrWithTimeArray1.from_json(string) json.value.should be_a(Array(Time)) json.value.should eq([Time.unix(1459859781)]) json.to_json.should eq(string) end it "uses converter instance with nested converter metaclass" do string = %({"value":[1459859781]}) json = JSONAttrWithTimeArray2.from_json(string) json.value.should be_a(Array(Time)) json.value.should eq([Time.unix(1459859781)]) json.to_json.should eq(string) end it "uses converter instance with nested converter instance" do string = %({"value":["2014-10-31 23:37:16"]}) json = JSONAttrWithTimeArray3.from_json(string) json.value.should be_a(Array(Time)) json.value.map(&.to_s).should eq(["2014-10-31 23:37:16 UTC"]) json.to_json.should eq(string) end end describe JSON::HashValueConverter do it "uses converter metaclass" do string = %({"value":{"foo":1459859781}}) json = JSONAttrWithTimeHash1.from_json(string) json.value.should be_a(Hash(String, Time)) json.value.should eq({"foo" => Time.unix(1459859781)}) json.to_json.should eq(string) end it "uses converter instance with nested converter metaclass" do string = %({"value":{"foo":1459859781}}) json = JSONAttrWithTimeHash2.from_json(string) json.value.should be_a(Hash(String, Time)) json.value.should eq({"foo" => Time.unix(1459859781)}) json.to_json.should eq(string) end it "uses converter instance with nested converter instance" do string = %({"value":{"foo":"2014-10-31 23:37:16"}}) json = JSONAttrWithTimeHash3.from_json(string) json.value.should be_a(Hash(String, Time)) json.value.transform_values(&.to_s).should eq({"foo" => "2014-10-31 23:37:16 UTC"}) json.to_json.should eq(string) end end it "parses raw value from int" do string = %({"value":123456789123456789123456789123456789}) json = JSONAttrWithRaw.from_json(string) json.value.should eq("123456789123456789123456789123456789") json.to_json.should eq(string) end it "parses raw value from float" do string = %({"value":123456789123456789.123456789123456789}) json = JSONAttrWithRaw.from_json(string) json.value.should eq("123456789123456789.123456789123456789") json.to_json.should eq(string) end it "parses raw value from object" do string = %({"value":[null,true,false,{"x":[1,1.5]}]}) json = JSONAttrWithRaw.from_json(string) 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 = JSONAttrWithRoot.from_json(json) result.result.should be_a(Array(JSONAttrPerson)) result.result.first.name.should eq "Batman" result.to_json.should eq(json) end it "parses with nilable root" do json = %({"result":null}) result = JSONAttrWithNilableRoot.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 = JSONAttrWithNilableRootEmitNull.from_json(json) result.result.should be_nil result.to_json.should eq(json) end it "parses nilable union" do obj = JSONAttrValue(Int32?).from_json(%({"value": 1})) obj.value.should eq(1) obj.to_json.should eq(%({"value":1})) obj = JSONAttrValue(Int32?).from_json(%({"value": null})) obj.value.should be_nil obj.to_json.should eq(%({})) obj = JSONAttrValue(Int32?).from_json(%({})) obj.value.should be_nil obj.to_json.should eq(%({})) end describe "parses JSON with presence markers" do it "parses person with absent attributes" do json = JSONAttrWithPresence.from_json(%({"first_name": null})) json.first_name.should be_nil json.first_name_present?.should be_true json.last_name.should be_nil json.last_name_present?.should be_false end end describe "serializes JSON with presence markers and ignore_serialize" do context "ignore_serialize is set to a method which returns true when value is nil or empty string" do it "ignores field when value is empty string" do json = JSONAttrWithPresenceAndIgnoreSerialize.from_json(%({"first_name": ""})) json.first_name_present?.should be_true json.to_json.should eq(%({})) end it "ignores field when value is nil" do json = JSONAttrWithPresenceAndIgnoreSerialize.from_json(%({"first_name": null})) json.first_name_present?.should be_true json.to_json.should eq(%({})) end end context "ignore_serialize is set to conditional expressions 'last_name.nil? && !last_name_present?'" do it "emits null when value is null and @last_name_present is true" do json = JSONAttrWithPresenceAndIgnoreSerialize.from_json(%({"last_name": null})) json.last_name_present?.should be_true json.to_json.should eq(%({"last_name":null})) end it "does not emit null when value is null and @last_name_present is false" do json = JSONAttrWithPresenceAndIgnoreSerialize.from_json(%({})) json.last_name_present?.should be_false json.to_json.should eq(%({})) end it "emits field when value is not nil and @last_name_present is false" do json = JSONAttrWithPresenceAndIgnoreSerialize.new(last_name: "something") json.last_name_present?.should be_false json.to_json.should eq(%({"last_name":"something"})) end it "emits field when value is not nil and @last_name_present is true" do json = JSONAttrWithPresenceAndIgnoreSerialize.from_json(%({"last_name":"something"})) json.last_name_present?.should be_true json.to_json.should eq(%({"last_name":"something"})) end end end describe "with query attributes" do it "defines query getter" do json = JSONAttrWithQueryAttributes.from_json(%({"foo": true})) json.foo?.should be_true json.bar?.should be_false end it "defines query getter with class restriction" do {% begin %} {% methods = JSONAttrWithQueryAttributes.methods %} {{ methods.find(&.name.==("foo?")).return_type }}.should eq(Bool) {{ methods.find(&.name.==("bar?")).return_type }}.should eq(Bool) {% end %} end it "defines non-query setter and presence methods" do json = JSONAttrWithQueryAttributes.from_json(%({"foo": false})) json.bar_present?.should be_false json.bar = true json.bar?.should be_true end it "maps non-query attributes" do json = JSONAttrWithQueryAttributes.from_json(%({"foo": false, "is_bar": false})) json.bar_present?.should be_true json.bar?.should be_false json.bar = true json.to_json.should eq(%({"foo":false,"is_bar":true})) end it "raises if non-nilable attribute is nil" do error_message = <<-'MSG' Missing JSON attribute: foo parsing JSONAttrWithQueryAttributes#foo at line 1, column 1 MSG ex = expect_raises ::JSON::SerializableError, error_message do JSONAttrWithQueryAttributes.from_json(%({"is_bar": true})) end ex.location.should eq({1, 1}) ex.attribute.should eq "foo" end it "raises with key as attribute if non-nilable attribute is nil" do error_message = <<-'MSG' Missing JSON attribute: is_foo parsing JSONAttrWithKeyQueryAttribute#is_foo at line 1, column 1 MSG ex = expect_raises ::JSON::SerializableError, error_message do JSONAttrWithKeyQueryAttribute.from_json(%({})) end ex.location.should eq({1, 1}) ex.attribute.should eq "is_foo" end end describe "BigDecimal" do it "parses json string with BigDecimal" do json = JSONAttrValue(BigDecimal).from_json(%({"value": "10.05"})) json.value.should eq(BigDecimal.new("10.05")) end it "parses large json ints with BigDecimal" do json = JSONAttrValue(BigDecimal).from_json(%({"value": 9223372036854775808})) json.value.should eq(BigDecimal.new("9223372036854775808")) end it "parses json float with BigDecimal" do json = JSONAttrValue(BigDecimal).from_json(%({"value": 10.05})) json.value.should eq(BigDecimal.new("10.05")) end it "parses large precision json floats with BigDecimal" do json = JSONAttrValue(BigDecimal).from_json(%({"value": 0.00045808999999999997})) json.value.should eq(BigDecimal.new("0.00045808999999999997")) end end it "parses 128-bit integer" do json = JSONAttrValue(Int128).from_json(%({"value": #{Int128::MAX}})) json.value.should eq Int128::MAX end describe "work with module and inheritance" do it { JSONAttrModuleTest.from_json(%({"phoo": 20})).to_tuple.should eq({10, 20}) } it { JSONAttrModuleTest.from_json(%({"phoo": 20})).to_tuple.should eq({10, 20}) } it { JSONAttrModuleTest2.from_json(%({"phoo": 20, "bar": 30})).to_tuple.should eq({10, 20, 30}) } it { JSONAttrModuleTest2.from_json(%({"bar": 30, "moo": 40})).to_tuple.should eq({40, 15, 30}) } end it "works together with yaml" do person = JSONAttrPersonWithYAML.new("Vasya", 30) person.to_json.should eq "{\"name\":\"Vasya\",\"age\":30}" person.to_yaml.should eq "---\nname: Vasya\nage: 30\n" JSONAttrPersonWithYAML.from_json(person.to_json).should eq person JSONAttrPersonWithYAML.from_yaml(person.to_yaml).should eq person end it "yaml and json with after_initialize hook" do person = JSONAttrPersonWithYAMLInitializeHook.new("Vasya", 30) person.msg.should eq "Hello Vasya" person.to_json.should eq "{\"name\":\"Vasya\",\"age\":30}" person.to_yaml.should eq "---\nname: Vasya\nage: 30\n" JSONAttrPersonWithYAMLInitializeHook.from_json(person.to_json).msg.should eq "Hello Vasya" JSONAttrPersonWithYAMLInitializeHook.from_yaml(person.to_yaml).msg.should eq "Hello Vasya" end it "json with selective serialization" do person = JSONAttrPersonWithSelectiveSerialization.new("Vasya", "P@ssw0rd") person.to_json.should eq "{\"name\":\"Vasya\",\"generated\":\"generated-internally\"}" person_json = "{\"name\":\"Vasya\",\"generated\":\"should not set\",\"password\":\"update\"}" person = JSONAttrPersonWithSelectiveSerialization.from_json(person_json) person.generated.should eq "generated-internally" person.password.should eq "update" end describe "use_json_discriminator" do it "deserializes with discriminator" do point = JSONShape.from_json(%({"type": "point", "x": 1, "y": 2})).as(JSONPoint) point.x.should eq(1) point.y.should eq(2) circle = JSONShape.from_json(%({"type": "circle", "x": 1, "y": 2, "radius": 3})).as(JSONCircle) circle.x.should eq(1) circle.y.should eq(2) circle.radius.should eq(3) end it "raises if missing discriminator" do expect_raises(::JSON::SerializableError, "Missing JSON discriminator field 'type'") do JSONShape.from_json("{}") end end it "raises if unknown discriminator value" do expect_raises(::JSON::SerializableError, %(Unknown 'type' discriminator value: "unknown")) do JSONShape.from_json(%({"type": "unknown"})) end end it "deserializes with variable discriminator value type" do object_number = JSONVariableDiscriminatorValueType.from_json(%({"type": 0})) object_number.should be_a(JSONVariableDiscriminatorNumber) object_string = JSONVariableDiscriminatorValueType.from_json(%({"type": "1"})) object_string.should be_a(JSONVariableDiscriminatorString) object_bool = JSONVariableDiscriminatorValueType.from_json(%({"type": true})) object_bool.should be_a(JSONVariableDiscriminatorBoolTrue) object_bool = JSONVariableDiscriminatorValueType.from_json(%({"type": false})) object_bool.should be_a(JSONVariableDiscriminatorBoolFalse) object_enum = JSONVariableDiscriminatorValueType.from_json(%({"type": 4})) object_enum.should be_a(JSONVariableDiscriminatorEnum) object_enum = JSONVariableDiscriminatorValueType.from_json(%({"type": 18})) object_enum.should be_a(JSONVariableDiscriminatorEnum8) end it "deserializes with discriminator, strict recursive type" do foo = JSONStrictDiscriminator.from_json(%({"type": "foo"})) foo = foo.should be_a(JSONStrictDiscriminatorFoo) bar = JSONStrictDiscriminator.from_json(%({"type": "bar", "x": {"type": "foo"}, "y": {"type": "foo"}})) bar = bar.should be_a(JSONStrictDiscriminatorBar) bar.x.should be_a(JSONStrictDiscriminatorFoo) bar.y.should be_a(JSONStrictDiscriminatorFoo) end it "deserializes with discriminator, another recursive type, fixes: #13429" do c = JsonDiscriminatorBug::Base.from_json %q({"type": "c", "source": {"type": "a"}, "value": 2}) c.as(JsonDiscriminatorBug::C).value.should eq 2 c = JsonDiscriminatorBug::Base.from_json %q({"type": "c", "source": {"type": "a"}}) c.as(JsonDiscriminatorBug::C).value.should eq 1 end end describe "Enum::ValueConverter.from_json" do it "normal enum" do JSONAttrWithEnumValue.from_json(%({"value": 0})).value.should eq(JSONSerializableEnum::Zero) JSONAttrWithEnumValue.from_json(%({"value": 1})).value.should eq(JSONSerializableEnum::One) JSONAttrWithEnumValue.from_json(%({"value": 2})).value.should eq(JSONSerializableEnum::Two) JSONAttrWithEnumValue.from_json(%({"value": 3})).value.should eq(JSONSerializableEnum::OneHundred) expect_raises(JSON::ParseException, %(Expected Int but was String)) do JSONAttrWithEnumValue.from_json(%({"value": "3"})) end expect_raises(JSON::ParseException, %(Unknown enum JSONSerializableEnum value: 4)) do JSONAttrWithEnumValue.from_json(%({"value": 4})) end expect_raises(JSON::ParseException, %(Unknown enum JSONSerializableEnum value: -1)) do JSONAttrWithEnumValue.from_json(%({"value": -1})) end expect_raises(JSON::ParseException, %(Expected Int but was String)) do JSONAttrWithEnumValue.from_json(%({"value": ""})) end expect_raises(JSON::ParseException, "Expected Int but was String") do JSONAttrWithEnumValue.from_json(%({"value": "one"})) end expect_raises(JSON::ParseException, "Expected Int but was BeginObject") do JSONAttrWithEnumValue.from_json(%({"value": {}})) end expect_raises(JSON::ParseException, "Expected Int but was BeginArray") do JSONAttrWithEnumValue.from_json(%({"value": []})) end end it "flag enum" do JSONAttrWithFlagEnumValue.from_json(%({"value": 0})).value.should eq(JSONSerializableFlagEnum::None) JSONAttrWithFlagEnumValue.from_json(%({"value": 1})).value.should eq(JSONSerializableFlagEnum::One) JSONAttrWithFlagEnumValue.from_json(%({"value": 2})).value.should eq(JSONSerializableFlagEnum::Two) JSONAttrWithFlagEnumValue.from_json(%({"value": 4})).value.should eq(JSONSerializableFlagEnum::OneHundred) JSONAttrWithFlagEnumValue.from_json(%({"value": 5})).value.should eq(JSONSerializableFlagEnum::OneHundred | JSONSerializableFlagEnum::One) JSONAttrWithFlagEnumValue.from_json(%({"value": 7})).value.should eq(JSONSerializableFlagEnum::All) expect_raises(JSON::ParseException, %(Unknown enum JSONSerializableFlagEnum value: 8)) do JSONAttrWithFlagEnumValue.from_json(%({"value": 8})) end expect_raises(JSON::ParseException, %(Unknown enum JSONSerializableFlagEnum value: -1)) do JSONAttrWithFlagEnumValue.from_json(%({"value": -1})) end expect_raises(JSON::ParseException, %(Expected Int but was String)) do JSONAttrWithFlagEnumValue.from_json(%({"value": ""})) end expect_raises(JSON::ParseException, "Expected Int but was String") do JSONAttrWithFlagEnumValue.from_json(%({"value": "one"})) end expect_raises(JSON::ParseException, "Expected Int but was BeginObject") do JSONAttrWithFlagEnumValue.from_json(%({"value": {}})) end expect_raises(JSON::ParseException, "Expected Int but was BeginArray") do JSONAttrWithFlagEnumValue.from_json(%({"value": []})) end end end describe "Enum::ValueConverter.to_json" do it "normal enum" do klass = JSONAttrWithEnumValue klass.new(JSONSerializableEnum::One).to_json.should eq %({"value":1}) klass.from_json(klass.new(JSONSerializableEnum::One).to_json).value .should eq(JSONSerializableEnum::One) klass.new(JSONSerializableEnum::OneHundred).to_json.should eq %({"value":3}) klass.from_json(klass.new(JSONSerializableEnum::OneHundred).to_json).value .should eq(JSONSerializableEnum::OneHundred) # undefined members can't be parsed back because the standard converter only accepts # named members klass.new(JSONSerializableEnum.new(42)).to_json.should eq %({"value":42}) end it "flag enum" do klass = JSONAttrWithFlagEnumValue klass.new(JSONSerializableFlagEnum::One).to_json.should eq %({"value":1}) klass.from_json(klass.new(JSONSerializableFlagEnum::One).to_json).value .should eq(JSONSerializableFlagEnum::One) klass.new(JSONSerializableFlagEnum::OneHundred).to_json.should eq %({"value":4}) klass.from_json(klass.new(JSONSerializableFlagEnum::OneHundred).to_json).value .should eq(JSONSerializableFlagEnum::OneHundred) combined = JSONSerializableFlagEnum::OneHundred | JSONSerializableFlagEnum::One klass.new(combined).to_json.should eq %({"value":5}) klass.from_json(klass.new(combined).to_json).value.should eq(combined) klass.new(JSONSerializableFlagEnum::None).to_json.should eq %({"value":0}) klass.from_json(klass.new(JSONSerializableFlagEnum::None).to_json).value .should eq(JSONSerializableFlagEnum::None) klass.new(JSONSerializableFlagEnum::All).to_json.should eq %({"value":7}) klass.from_json(klass.new(JSONSerializableFlagEnum::All).to_json).value .should eq(JSONSerializableFlagEnum::All) klass.new(JSONSerializableFlagEnum.new(42)).to_json.should eq %({"value":42}) end end describe "namespaced classes" do it "lets default values use the object's own namespace" do request = JSONNamespace::FooRequest.from_json(%({"foo":{}})) request.foo.id.should eq "id:foo" request.bar.id.should eq "id:bar" end end it "fixes #13337" do JSONSomething.from_json(%({"value":{}})).value.should_not be_nil end it "works when type has constructor with double splat parameter (#16140)" do JSONInitializeOpts.from_json(%({"value":123})).value.should eq(123) end it "supports generic type variables in converters" do JSONAttrWithGenericConverter(Time::EpochConverter).from_json(%({"value":1459859781})).value.should eq(Time.unix(1459859781)) end it "fixes #16141" do SerializableFoo.find.from_json("{}").should be_a(SerializableBar) end end