JSON2.jl icon indicating copy to clipboard operation
JSON2.jl copied to clipboard

custom JSON encoding/decoding

Open visr opened this issue 7 years ago • 3 comments

I'm trying to implement some custom JSON encoding/decoding for a float (to support this).

# default behavior is compliant but not what I want
using JSON2

JSON2.write(1.2)  # => "1.2", ok
JSON2.write(NaN)  # => "null", want "\"NaN\""
JSON2.write(Inf)  # => "null", want "\"Infinity\""

So I attempt to customize it with JSON2.@format

# struct I want to encode
struct Metadata{T}
    something::T
    fill_value::Union{T, Nothing}
end

# new struct to implement custom behavior on
struct FillValue{T}
    val::T
end

Base.convert(FillValue, v) = FillValue(v)

# tell JSON2 to use this new struct for fill_value
JSON2.@format Metadata begin
    fill_value => (;jsontype=FillValue)
end

With the following custom behavior

JSON2.write(fill_value::FillValue) = JSON2.write(fill_value.val)

function JSON2.write(fill_value::FillValue{<:AbstractFloat})
    v = fill_value.val
    if isnan(v)
        string('"', v, '"')
    elseif isinf(v)
        string('"', v, "inity\"")
    else
        JSON2.write(v)
    end
end

So now for FillValue alone I get the desired behavior:

JSON2.write(FillValue(1.2))  # => "1.2"
JSON2.write(FillValue(NaN))  # => "\"NaN\""
JSON2.write(FillValue(Inf))  # => "\"Infinity\""

But unfortunately not for the entire Metadata struct:

JSON2.write(Metadata(1.5, 2.5))  # => "{\"something\":1.5,\"fill_value\":{\"val\":2.5}}"
JSON2.write(Metadata(1.5, Inf))  # => "{\"something\":1.5,\"fill_value\":{\"val\":null}}"

What is the right approach here? Ideally I was thinking about having support for something like this:

read_fill_value(v) = # custom logic for "Infinity" to Inf
write_fill_value(v) = # custom logic for Inf to "Infinity" (and beyond)

JSON2.@format Metadata begin
    fill_value => (;read=read_fill_value, write=write_fill_value)
end

visr avatar Sep 07 '18 14:09 visr

Super close; the functions you actually want to override are JSON2.write(io::IO, fill_value::FillValue) and JSON2.read(io::IO, ::Type{FillValue{T}}) where {T}; similar to how you override Base.show(io::IO, x::FillValue) in Base.

quinnj avatar Sep 13 '18 05:09 quinnj

Great, thanks! If you want I can add a simple example to the README. But with the simple example below I also cannot read back in as I want.

# subtract 1 from T.a upon encoding, add it back on decoding JSON

using JSON2

struct T
    a::Int
    b::Int
end

struct IntMinus1
    val::Int
end

JSON2.@format T begin
    a => (jsontype=IntMinus1,)
end

JSON2.write(io::IO, a::IntMinus1) = JSON2.write(io, a.val - 1)
JSON2.read(io::IO, ::Type{IntMinus1}) = JSON2.read(io, Int) + 1
Base.convert(IntMinus1, v) = IntMinus1(v)

JSON2.write(IntMinus1(1))  # => "0"
JSON2.read(IOBuffer("0"), IntMinus1)  # => 1

js = JSON2.write(T(1, 0))  # => {"a":0,"b":0}
JSON2.read(IOBuffer(js), T)  # throws error below
ArgumentError: type does not have a definite number of fields
fieldcount(::Any) at reflection.jl:621
#s15#9(::Any, ::Any, ::Any) at JSON2.jl:181
(::Core.GeneratedFunctionStub)(::Any, ::Vararg{Any,N} where N) at boot.jl:506

visr avatar Sep 16 '18 13:09 visr

Oh dear, this one is a bit embarrassing. The problem is that you named your struct T, which conflicts with the generated function argument type in the default JSON read. You can see it work if you rename your struct to T2. I'll see what needs to happen so we don't run into this.

quinnj avatar Sep 18 '18 06:09 quinnj