I have the following problem migrating from JSON3.jl to JSON.jl v1:
julia> struct MyStruct
a::Int
b::String
# MyStruct(a::Int) = new(a, string(a)) this won't work even with JSON3
MyStruct(a::Int, ::Nothing = nothing) = new(a, string(a))
end
julia> JSON.parse("""{"a": 42}""", MyStruct)
ERROR: TypeError: in typeassert, expected String, got a value of type Nothing
...
julia> JSON3.read("""{"a": 42}""", MyStruct) # works out of the box
MyStruct2(42, "42")
I haven’t been able to figure out how to make this work. There is StructUtils.@defaults, which works for construction but not for parsing:
julia> @defaults struct MyStruct2
a::Int
b::String = string(a)
end
julia> MyStruct2(1)
MyStruct2(1, "1")
julia> JSON.parse("""{"a": 42}""", MyStruct2)
ERROR: UndefVarError: `a` not defined in `Main`
Suggestion: check for spelling errors or missing imports.
...
I think JSON.jl wants you to represent such a type with explicit option, a la
struct MyStruct
a::Int
b::Union{String, Nothing, Missing} #nothing: value not set in json; missing: explicit null but key is present
end
If you want to conflate {"a":1} and {"a":1, "b":null} then you can use simple Union{Nothing, String}.
That being said, you should open an issue on StructUtils. The generated default constructors and StructUtils.fielddefaults should ideally have the same behavior on this.
(I fear that this will be not be entirely trivial to get right without breakage, especially side-effects and their ordering of default argument argument evaluation needs to be the same between “default arguments for function / type constructor” and “construction via JSON.parse / StructUtils.fielddefaults”).
julia> @macroexpand @defaults struct A
x::Int
y::Int = x
end
quote
begin
$(Expr(:meta, :doc))
struct A
#= REPL[3]:2 =#
x::Int
#= REPL[3]:3 =#
y::Int
end
end
function A(x)
#= REPL[3]:1 =#
return A(x, x)
end
StructUtils.fielddefaults(::StructUtils.StructStyle, ::Type{<:A}) = begin
y = x
return (; $(Expr(:(=), :y, :y)))
end
A
end
Thanks for the suggestions. Unless I misunderstood, even if I change the field type to allow Nothing, I would not get the computed value by parsing. Moreover, I would then need to account for the possibility of Nothing in all downstream code, or alternatively convert it to a different type before further use.
So far, I’ve been working around this issue by doing custom StructUtils.make functions such as
function StructUtils.make(::Type{MyStruct2}, x)
if haskey(x, :b)
return MyStruct2(x.a, x.b)
else
return MyStruct2(x.a, string(x.a))
end
end
i.e. getting rid of the custom internal constructor and pattern matching “by hand”. But this gets tedious if you have dozens of structs you want to serialize, and multiple computed fields, and are changing them frequently. I would love to only need to update the struct definition. For more mature interfaces I’m not opposed to write a shim such that the serializable structure can deviate in more ways from the internal one.
It appears that you understood perfectly fine, and it was me who misunderstood your original question.
If that works, then it should be possible to improve StructUtils.@defaults to also generate exactly that StructUtils.make for you. Ideally in a PR / upstream, but you can also write your own.
I think this boils down to “StructUtils.fielddefaults as a mechanism / abstraction cannot work, scrap it”, which is what I meant with “this may not be entirely trivial to get right without breakage”.
I don’t know the implementation details too well, but there could maybe also be a stage before mapping 1:1 to fields. Somehow JSON3.jl got it done as well, and I don’t see it mentioned anywhere that JSON.jl v1 intentionally removed that feature.