The Crystal Programming Language Forum

Allow EpochConverter to receive null json value

First, sorry that I am still on Crystal v0.30, and still using JSON.mapping. But likely the issue I am having does not have to do with this.

I have field called timecreated in the JSON that I send to server. The JSON.mapping code is

JSON.mapping(
     timecreated: {type: Time?, converter: Time::EpochConverter}
)

If I pass a JSON value in my postdata where timecreated field is null then I get an error saying Expected Int but was Null. This is despite declaring the field as nilable. I thought, this perhaps has to do with Time::EpochConverter not being able to handle null JSON value. (If I pass an integer for timecreated, I don’t get any error). So, I tried to extend Time::EpochConverter to allow for null values by adding the following code to my src file.

module Time::EpochConverter
    def self.from_json(value : JSON::PullParser) : Time|Nil
        if (value.read_int_or_null)
            Time.unix(value.read_int)
        else
            nil
        end
    end
end

With this code added, null no longer throws an error. However, if I pass an integer JSON value to the timecreated field, I get the following error:
Expected Int but was String

I have two questions: (1) Why is it throwing this Expected Int but was String
error because of the code I added?
(2) How do I allow EpochConverter to accept null json values.

Thank you.

Given JSON.mapping is deprecated, I would just change your implementation to use JSON::Serializable, which doesn’t seem to have this problem: https://play.crystal-lang.org/#/r/9bb0.

While on the other-hand, using JSON.mapping, you run into this problem: https://play.crystal-lang.org/#/r/9bb2.

Thank you @Blacksmoke16. I will convert it to JSON::Serializable and try with that.

The following should work:

require "json"

class MyObject
  JSON.mapping(
    created_at: {type: Time?, converter: Time::EpochConverter, nilable: true}
  )
end

pp MyObject.from_json %({"created_at":null})
pp MyObject.from_json %({"created_at":1592929230})

It seems that the logic to wrap the pull_parser with a read_null_or is not handled when the type is nilable but there is no nilable explicit named argument.

And just because it’s fun, this would be another alternative

class MyObject
  JSON.mapping(
    created_at: {type: Time?, converter: NilableConverter(Time::EpochConverter)}
  )
end

module NilableConverter(T)
  def self.from_json(value : JSON::PullParser)
    value.read_null_or do
      T.from_json(value)
    end
  end
end

Thank you @bcardiff. I will try this. I tried using JSON::Serializable. Specifically, using

@[JSON::Field(emit_null: true, converter: Time::EpochConverter)]
property timecreated : Time?

But with that I get the error:

Expected String but was Int

I don’t know why it is expecting a String. Could be because I am using an older version of Crystal. But let me try your solution. Thank you.

The above solution worked great. Thank you!