StructTypes.Mutable() does not call the inner constructor

using JSON3
using StructTypes

Base.@kwdef mutable struct MyType2
    c::Float64 = 3.0

    MyType2(c) = c < 0.0 ? error("bar") : new(c)
end

Base.@kwdef mutable struct MyType
    a::Int = 1
    b::MyType2 = MyType2(3.0)

    MyType(a,b) = a > 10 ? error("foo") : new(a,b)
end

StructTypes.StructType(::Type{MyType}) = StructTypes.Mutable()
StructTypes.StructType(::Type{MyType2}) = StructTypes.Mutable()

json_string = """{"a":11, "b":{"c":-3.0}}"""
hello_world = JSON3.read(json_string, MyType; parsequoted= true)

Dear community,
here is the ref to the issue i opened:
StructTypes.Mutable() does not call the inner constructor · Issue #266 · quinnj/JSON3.jl · GitHub

Lets assume i have a two mutable structs of MyType2 which is then nested into MyType.
I expect JSON3.read to trow an error since json_string has to invalid input according to inner constructor. My understand is that StuctTypes.Muable() calls an emtpy constructor and then mutates the arguements in order to deal with missing arguments in the json_string. I want it to be able top handly missing arguments but still call the innerconstructor to validate the input.
Is there a way to throw theses errors using StructTypes while parsing the “json_string”.
Due to legacy reasons i don’t want to change the structs to be immutable.
Tips are very appreciated, thank you for your support in advance.

best regards
BlondDon

Disclaimer: not a JSON3 contributor/maintainer here.

The issue raised by you has less to do with JSON3 or StructTypes packages - it touches the more general discussion of serialization/deserialization, with a particular focus on mutable data types.

Technically speaking, you can always do this:

using JSON3
using StructTypes

@kwdef mutable struct MyType2
    c::Float64 = 3.0

    MyType2(c) = c < 0.0 ? error("bar") : new(c)
end

StructTypes.StructType(::Type{MyType}) = StructTypes.Mutable()

Base.@kwdef mutable struct MyType
    a::Int = 1
    b::MyType2 = MyType2(3.0)

    MyType(a, b) = a > 10 ? error("foo") : new(a, b)
end

StructTypes.StructType(::Type{MyType2}) = StructTypes.Mutable()

v = MyType(9, MyType2(3.0))

v.a = 11
v.b.c = -3.0

serialized = JSON3.write(v)
deserialized = JSON3.read(serialized, MyType; parsequoted=true)

@info v
@info deserialized

Now, v exists in your session in a state not achievable by the direct usage of the constructor. And that is totally legitimate for a mutable struct: you can have restrictions in the constructor but mutate your struct fields at a later time (imagine the scenario where you initiate a “person” at age 0 - but increase the mutable field age as the person grows older).

You might what to serialize the object v and deserialize it later.

The deserialization has nothing to do with constructing a new object (via new()) - the deserialization ensures that a previous state of your object will be restored: that is part of the job of StructTypes - and subsequently of the JSON3 package.

Imposing the restrictions from new() would directly violate the very meaning of restoring an object to a previous state. And would break the package deserialization functionality.

So, when you say,

you basically say that you expect JSON3 to give up the deserialization for mutable structs and do something else.

A solution for your issue might consist in building your own validator and checking the deserialized objects (and even instantiating the objects to the desired values/state if the check fails).

Also, if you don’t want some states to be representable for your struct type, you can do something like this:

@kwdef mutable struct MyType2
    c::Float64 = 3.0

    MyType2(c) = c < 0.0 ? error("bar") : new(c)
end

function Base.setproperty!(v::MyType2, field::Symbol, value)
    field == :c && value < 0.0 && error("bar")
    setfield!(v, field, value)
end

mt2 = MyType2(3.0)

# this will fail now
mt2.c = -3.0

Imposing such constraint will at least prevent “invalid” states from being reached in your program - and you’ll never actually end up doing serialization of the “illegal state.”

P. S. In the case of the immutable structs, the premise is that the field values will not change once the struct is instantiated - consequently, you will not be able to serialize some “illegal state” of your struct (thus, there is no question of what is going to happen at the deserialization time).

Now, there can be both custom serialization and deserialization behaviors implemented. In such scenarios can be totally legitimate to impose any restrictions you want (both related to the new() constraints or something else - even arbitrary).

2 Likes

Hi @algunion

Thanks for your reply. Unfortunately the serialization step happens outside of julia by a client which then sends me the JSON. My goal is to detect the illegal state during deserialization(while constructing the julia object).
I tried the other route of making all struct immuatable, but now there seems to be issue with the @kwdef since now i have to pass all parameters even if i have a @kwdef in place for everything. In the example below the “a” parameter is not passed in the json, but should default to 1 as the kwdef defines. Is there a way to make it work this way?
If clients have to pass all parameters, this would be a unnecessary overhead as well as a breaking API change for us.

using JSON3
using StructTypes

Base.@kwdef struct MyType2
    c::Float64 = 3.0

    MyType2(c) = c < 0.0 ? error("bar") : new(c)
end

Base.@kwdef struct MyType
    a::Int = 1
    b::MyType2 = MyType2(3.0)

    MyType(a,b) = a > 10 ? error("foo") : new(a,b)
end

StructTypes.StructType(::Type{MyType}) = StructTypes.Struct()
StructTypes.StructType(::Type{MyType2}) = StructTypes.Struct()

json_string = """{"b":{"c":3.0}}"""
hello_world = JSON3.read(json_string, MyType; parsequoted= true)

You’ll have to deal with the “missing” fields. I hope this helps:

using JSON3
using StructTypes

Base.@kwdef struct MyType2
    c::Float64 = 3.0

    MyType2(c) = c < 0.0 ? error("bar") : new(c)
end

Base.@kwdef struct MyType
    a::Int = 1
    b::MyType2 = MyType2(3.0)

    function MyType(a, b)
        !isnothing(a) && a > 10 && error("foo")
        isnothing(a) && return MyType(b=b)
        isnothing(b) && return MyType(a=a)
        new(a, b)
    end
end

StructTypes.StructType(::Type{MyType}) = StructTypes.Struct()
StructTypes.StructType(::Type{MyType2}) = StructTypes.Struct()

json_string = """{"b":{"c":3.0}}"""
JSON3.read(json_string, MyType; parsequoted=true)

json_string = """{"a": 2, "b":{"c":3.0}}"""
JSON3.read(json_string, MyType; parsequoted=true)

json_string = """{"a": 2}"""
JSON3.read(json_string, MyType; parsequoted=true)

The problem is that JSON3 doesn’t call your keyword-based constructors (defined via @kwdef). So you must do that yourself (see the adjusted code I pasted above).

1 Like