diff --git a/spec/std/json/serializable_spec.cr b/spec/std/json/serializable_spec.cr index 8df13e4f7280..2acd7f3373e1 100644 --- a/spec/std/json/serializable_spec.cr +++ b/spec/std/json/serializable_spec.cr @@ -257,6 +257,29 @@ class JSONAttrWithPresence 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 @@ -811,6 +834,48 @@ describe "JSON mapping" do 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})) diff --git a/src/json/serialization.cr b/src/json/serialization.cr index d925374466ff..450a15287236 100644 --- a/src/json/serialization.cr +++ b/src/json/serialization.cr @@ -60,7 +60,7 @@ module JSON # # `JSON::Field` properties: # * **ignore**: if `true` skip this field in serialization and deserialization (by default false) - # * **ignore_serialize**: if `true` skip this field in serialization (by default false) + # * **ignore_serialize**: If truthy, skip this field in serialization (default: `false`). The value can be any Crystal expression and is evaluated at runtime. # * **ignore_deserialize**: if `true` skip this field in deserialization (by default false) # * **key**: the value of the key in the json object (by default the name of the instance variable) # * **root**: assume the value is inside a JSON object with a given key (see `Object.from_json(string_or_io, root)`) @@ -270,14 +270,15 @@ module JSON {% properties = {} of Nil => Nil %} {% for ivar in @type.instance_vars %} {% ann = ivar.annotation(::JSON::Field) %} - {% unless ann && (ann[:ignore] || ann[:ignore_serialize]) %} + {% unless ann && (ann[:ignore] || ann[:ignore_serialize] == true) %} {% properties[ivar.id] = { - type: ivar.type, - key: ((ann && ann[:key]) || ivar).id.stringify, - root: ann && ann[:root], - converter: ann && ann[:converter], - emit_null: (ann && (ann[:emit_null] != nil) ? ann[:emit_null] : emit_nulls), + type: ivar.type, + key: ((ann && ann[:key]) || ivar).id.stringify, + root: ann && ann[:root], + converter: ann && ann[:converter], + emit_null: (ann && (ann[:emit_null] != nil) ? ann[:emit_null] : emit_nulls), + ignore_serialize: ann && ann[:ignore_serialize], } %} {% end %} @@ -287,42 +288,49 @@ module JSON {% for name, value in properties %} _{{name}} = @{{name}} - {% unless value[:emit_null] %} - unless _{{name}}.nil? + {% if value[:ignore_serialize] %} + unless {{ value[:ignore_serialize] }} {% end %} - json.field({{value[:key]}}) do - {% if value[:root] %} - {% if value[:emit_null] %} - if _{{name}}.nil? - nil.to_json(json) - else + {% unless value[:emit_null] %} + unless _{{name}}.nil? + {% end %} + + json.field({{value[:key]}}) do + {% if value[:root] %} + {% if value[:emit_null] %} + if _{{name}}.nil? + nil.to_json(json) + else + {% end %} + + json.object do + json.field({{value[:root]}}) do {% end %} - json.object do - json.field({{value[:root]}}) do - {% end %} - - {% if value[:converter] %} - if _{{name}} - {{ value[:converter] }}.to_json(_{{name}}, json) - else - nil.to_json(json) - end - {% else %} - _{{name}}.to_json(json) - {% end %} - - {% if value[:root] %} - {% if value[:emit_null] %} + {% if value[:converter] %} + if _{{name}} + {{ value[:converter] }}.to_json(_{{name}}, json) + else + nil.to_json(json) end + {% else %} + _{{name}}.to_json(json) {% end %} + + {% if value[:root] %} + {% if value[:emit_null] %} + end + {% end %} + end end - end - {% end %} - end + {% end %} + end - {% unless value[:emit_null] %} + {% unless value[:emit_null] %} + end + {% end %} + {% if value[:ignore_serialize] %} end {% end %} {% end %}