Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow passing instance method or conditional expressions to option ignore_serialize on JSON::Field #11804

Merged
merged 10 commits into from
Mar 14, 2022
65 changes: 65 additions & 0 deletions spec/std/json/serializable_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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}))
Expand Down
86 changes: 48 additions & 38 deletions src/json/serialization.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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**: a Bool literal or instance method returning Bool or conditional expressions, skip this field in serialization if it is `true` after evaluation (by default false)
cyangle marked this conversation as resolved.
Show resolved Hide resolved
# * **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)`)
Expand Down Expand Up @@ -270,60 +270,70 @@ 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] %}
straight-shoota marked this conversation as resolved.
Show resolved Hide resolved
{%
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 %}
{% end %}

json.object do
{% for name, value in properties %}
_{{name}} = @{{name}}
{% unless value[:ignore_serialize] == true %}
straight-shoota marked this conversation as resolved.
Show resolved Hide resolved
_{{name}} = @{{name}}

{% 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 %}
{% unless value[:ignore_serialize].nil? || value[:ignore_serialize] == false %}
straight-shoota marked this conversation as resolved.
Show resolved Hide resolved
unless {{ value[:ignore_serialize] }}
{% end %}

json.object do
json.field({{value[:root]}}) do
{% unless value[:emit_null] %}
unless _{{name}}.nil?
{% end %}

{% if value[:converter] %}
if _{{name}}
{{ value[:converter] }}.to_json(_{{name}}, json)
else
nil.to_json(json)
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 %}

{% 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
{% else %}
_{{name}}.to_json(json)
{% end %}

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

{% unless value[:emit_null] %}
end
{% unless value[:ignore_serialize].nil? || value[:ignore_serialize] == false %}
straight-shoota marked this conversation as resolved.
Show resolved Hide resolved
end
{% end %}
{% end %}
{% end %}
on_to_json(json)
Expand Down