How to create a struct from a dict

I have a Dict from parsing a JSON message, and want to convert it into a struct.

This works:

using StaticArrays

const MyFloat = Float64

sys_state_dict = Dict(:time      => 0,
    :t_sim     => 0,
    :sys_state => 0,
    :e_mech    => 0,
    :orient    => [0.530989, 0.466959, -0.466959, -0.530989],
    :elevation => 1.2426,
    :azimuth   => 0,
    :l_tether  => 150,
    :v_reelout => 0,
    :force     => 559.847,
    :depower   => 0.25,
    :steering  => 0,
    :heading   => 0,
    :course    => 0,
    :v_app     => 12.5799,
    :vel_kite  => [0, 0, 0],
    :X         => Union{Float64, Int64}[0, 8.90221, 17.5546, 25.9174, 33.9673, 41.687, 49.0618, 49.2317, 50.7465, 50.4513, 50.4513],
    :Y         => Union{Float64, Int64}[0, 0, 0, 0, 0, 0, 0, 0, 0, 2.84247, -2.84247],
    :Z         => Union{Float64, Int64}[0, 23.41, 46.9136, 70.5217, 94.2383, 118.064, 142.0, 147.029, 149.031, 146.739, 146.739]
)

P = 11
mutable struct SysState{P}
    "time since start of simulation in seconds"
    time::Float64
    "time needed for one simulation timestep"
    t_sim::Float64
    "state of system state control"
    sys_state::Int16
    "mechanical energy [Wh]"
    e_mech::Float64
    "orientation of the kite (quaternion, order w,x,y,z)"
    orient::MVector{4, Float32}
    "elevation angle in radians"
    elevation::MyFloat
    "azimuth angle in radians"
    azimuth::MyFloat
    "tether length [m]"
    l_tether::MyFloat
    "reel out velocity [m/s]"
    v_reelout::MyFloat
    "tether force [N]"
    force::MyFloat
    "depower settings [0..1]"
    depower::MyFloat
    "steering settings [-1..1]"
    steering::MyFloat
    "heading angle in radian"
    heading::MyFloat
    "course angle in radian"
    course::MyFloat
    "norm of apparent wind speed [m/s]"
    v_app::MyFloat
    "velocity vector of the kite"
    vel_kite::MVector{3, MyFloat}
    "vector of particle positions in x"
    X::MVector{P, MyFloat}
    "vector of particle positions in y"
    Y::MVector{P, MyFloat}
    "vector of particle positions in z"
    Z::MVector{P, MyFloat}
end 

function sys_state_dict2struct(sys_state_dict)
    SysState{P}(sys_state_dict[:time],
        sys_state_dict[:t_sim],
        sys_state_dict[:sys_state],
        sys_state_dict[:e_mech],
        sys_state_dict[:orient],
        sys_state_dict[:elevation],
        sys_state_dict[:azimuth],
        sys_state_dict[:l_tether],
        sys_state_dict[:v_reelout],
        sys_state_dict[:force],
        sys_state_dict[:depower],
        sys_state_dict[:steering],
        sys_state_dict[:heading],
        sys_state_dict[:course],
        sys_state_dict[:v_app],
        sys_state_dict[:vel_kite],
        sys_state_dict[:X],
        sys_state_dict[:Y],
        sys_state_dict[:Z]
    )
end

sys_state_dict2struct(sys_state_dict)

But it is error prone, because if I make a change to the struct I also have to make a manual change to the function sys_state_dict2struct().

How can I avoid that?

Not working:

using StaticArrays, StructTypes, JSON3

const MyFloat = Float64

sys_state_dict = Dict(:time      => 0,
    :t_sim     => 0,
    :sys_state => 0,
    :e_mech    => 0,
    :orient    => [0.530989, 0.466959, -0.466959, -0.530989],
    :elevation => 1.2426,
    :azimuth   => 0,
    :l_tether  => 150,
    :v_reelout => 0,
    :force     => 559.847,
    :depower   => 0.25,
    :steering  => 0,
    :heading   => 0,
    :course    => 0,
    :v_app     => 12.5799,
    :vel_kite  => [0, 0, 0],
    :X         => Union{Float64, Int64}[0, 8.90221, 17.5546, 25.9174, 33.9673, 41.687, 49.0618, 49.2317, 50.7465, 50.4513, 50.4513],
    :Y         => Union{Float64, Int64}[0, 0, 0, 0, 0, 0, 0, 0, 0, 2.84247, -2.84247],
    :Z         => Union{Float64, Int64}[0, 23.41, 46.9136, 70.5217, 94.2383, 118.064, 142.0, 147.029, 149.031, 146.739, 146.739]
)

P = 11
mutable struct SysState{P}
    "time since start of simulation in seconds"
    time::Float64
    "time needed for one simulation timestep"
    t_sim::Float64
    "state of system state control"
    sys_state::Int16
    "mechanical energy [Wh]"
    e_mech::Float64
    "orientation of the kite (quaternion, order w,x,y,z)"
    orient::MVector{4, Float32}
    "elevation angle in radians"
    elevation::MyFloat
    "azimuth angle in radians"
    azimuth::MyFloat
    "tether length [m]"
    l_tether::MyFloat
    "reel out velocity [m/s]"
    v_reelout::MyFloat
    "tether force [N]"
    force::MyFloat
    "depower settings [0..1]"
    depower::MyFloat
    "steering settings [-1..1]"
    steering::MyFloat
    "heading angle in radian"
    heading::MyFloat
    "course angle in radian"
    course::MyFloat
    "norm of apparent wind speed [m/s]"
    v_app::MyFloat
    "velocity vector of the kite"
    vel_kite::MVector{3, MyFloat}
    "vector of particle positions in x"
    X::MVector{P, MyFloat}
    "vector of particle positions in y"
    Y::MVector{P, MyFloat}
    "vector of particle positions in z"
    Z::MVector{P, MyFloat}
end 


StructTypes.StructType(::Type{SysState{P}}) = StructTypes.Mutable()

function sys_state_dict2struct(sys_state_dict)
   JSON3.read(sys_state_dict, SysState{P})
end

sys_state_dict2struct(sys_state_dict)

This fails with the error message:

ERROR: LoadError: MethodError: no method matching read(::Dict{Symbol, Any}, ::Type{SysState{11}})

Closest candidates are:
  read(::StructTypes.NumberType, ::Any, ::Any, ::Any, ::Any, ::Type{Integer}; kw...)
   @ JSON3 ~/.julia/packages/JSON3/jSAdy/src/structs.jl:67
  read(::StructTypes.NumberType, ::Any, ::Any, ::Any, ::Any, ::Type{Real}; kw...)
   @ JSON3 ~/.julia/packages/JSON3/jSAdy/src/structs.jl:65
  read(::StructTypes.NumberType, ::Any, ::Any, ::Any, ::Any, ::Type{Union{Bool, T}}; kw...) where T<:Real
   @ JSON3 ~/.julia/packages/JSON3/jSAdy/src/structs.jl:61
  ...

Stacktrace:
 [1] sys_state_dict2struct(sys_state_dict::Dict{Symbol, Any})
   @ Main ~/repos/KiteViewers.jl/mwes/mwe_09.jl:72
 [2] top-level scope
   @ ~/repos/KiteViewers.jl/mwes/mwe_09.jl:75
 [3] include(fname::String)
   @ Base.MainInclude ./client.jl:489
 [4] top-level scope
   @ REPL[1]:1
in expression starting at /home/ufechner/repos/KiteViewers.jl/mwes/mwe_09.jl:75

Any idea?

If you are not set on using StructTypes something along these lines could work.
The code simply iterates over the names of all fields of your struct and sets them according to corresponding value in the Dict (assuming the key exists!!)

julia> module FooingDict

       mutable struct Foo
           a
           b
           c
           Foo()=new() # alternatively you could provide a constructor with default values
       end

       Foo(d::Dict) =
           let foo=Foo()
               for f in fieldnames(Foo)
                   setproperty!(foo, f, d[f])
               end
               foo
           end

       d = Dict(:a=>1, :b=>2, :c=>3)
       println(Foo(d))

       end;
Main.FooingDict.Foo(1, 2, 3)

This was new to me. It works now:

using StaticArrays, StructTypes, JSON3

const MyFloat = Float64

sys_state_dict = Dict(:time      => 0,
    :t_sim     => 0,
    :sys_state => 0,
    :e_mech    => 0,
    :orient    => [0.530989, 0.466959, -0.466959, -0.530989],
    :elevation => 1.2426,
    :azimuth   => 0,
    :l_tether  => 150,
    :v_reelout => 0,
    :force     => 559.847,
    :depower   => 0.25,
    :steering  => 0,
    :heading   => 0,
    :course    => 0,
    :v_app     => 12.5799,
    :vel_kite  => [0, 0, 0],
    :X         => Union{Float64, Int64}[0, 8.90221, 17.5546, 25.9174, 33.9673, 41.687, 49.0618, 49.2317, 50.7465, 50.4513, 50.4513],
    :Y         => Union{Float64, Int64}[0, 0, 0, 0, 0, 0, 0, 0, 0, 2.84247, -2.84247],
    :Z         => Union{Float64, Int64}[0, 23.41, 46.9136, 70.5217, 94.2383, 118.064, 142.0, 147.029, 149.031, 146.739, 146.739]
)

P = 11
mutable struct SysState{P}
    "time since start of simulation in seconds"
    time::Float64
    "time needed for one simulation timestep"
    t_sim::Float64
    "state of system state control"
    sys_state::Int16
    "mechanical energy [Wh]"
    e_mech::Float64
    "orientation of the kite (quaternion, order w,x,y,z)"
    orient::MVector{4, Float32}
    "elevation angle in radians"
    elevation::MyFloat
    "azimuth angle in radians"
    azimuth::MyFloat
    "tether length [m]"
    l_tether::MyFloat
    "reel out velocity [m/s]"
    v_reelout::MyFloat
    "tether force [N]"
    force::MyFloat
    "depower settings [0..1]"
    depower::MyFloat
    "steering settings [-1..1]"
    steering::MyFloat
    "heading angle in radian"
    heading::MyFloat
    "course angle in radian"
    course::MyFloat
    "norm of apparent wind speed [m/s]"
    v_app::MyFloat
    "velocity vector of the kite"
    vel_kite::MVector{3, MyFloat}
    "vector of particle positions in x"
    X::MVector{P, MyFloat}
    "vector of particle positions in y"
    Y::MVector{P, MyFloat}
    "vector of particle positions in z"
    Z::MVector{P, MyFloat}
    SysState{P}() = new() 
end 

SysState{P}(d::Dict) =
let ss=SysState{P}()
    for f in fieldnames(SysState{P})
        setproperty!(ss, f, d[f])
    end
    ss
end


# StructTypes.StructType(::Type{SysState{P}}) = StructTypes.Mutable()

function sys_state_dict2struct(sys_state_dict)
   SysState{P}(sys_state_dict)
end

sys_state_dict2struct(sys_state_dict)

Thanks a lot!

If you define your struct with Base.@kwdef you can do this:

sys_state_dict2struct(dict) = SysState{P}(; pairs(dict)...)
3 Likes

Here’s a general version:

struct BasicStruct
       a::Int
       b::Int
end

function conform(::Type{T}, d::Dict)::T where T
           struct_inputs = [d[field_name] for field_name in fieldnames(T)]
           return T(struct_inputs...)
end

dict_data = Dict(:a => 2, :b => 3)

> conform(BasicStruct, dict_data)
BasicStruct(2, 3)

This should work as long as the dict has a key of the right type for each field of the struct.

It works. Different methods have a different performance:

sys_state_dict2struct(dict) = SysState{P}(; pairs(dict)...)

gives:

@btime sys_state_dict2struct($sys_state_dict)
  2.638 μs (50 allocations: 3.58 KiB)

and

SysState{P}(d::Dict) =
let ss=SysState{P}()
    for f in fieldnames(SysState{P})
        setproperty!(ss, f, d[f])
    end
    ss
end

sys_state_dict2struct(sys_state_dict) = SysState{P}(sys_state_dict)

results in

julia> @btime sys_state_dict2struct($sys_state_dict)
  3.562 μs (48 allocations: 1.39 KiB)

Not so sure yet what is more important for me, low allocations or high speed.

Nevertheless thanks a lot!

2 Likes

Just curious, but what is the difference between this use case and your previous question?

My previous question assumed that the number of entries in the dict are smaller than the number of fields of the struct.

The key focus in the new question was how to make the conversion if all fields are present, but in a way that that changes to the struct propagate automatically (do not require any change in the function that does the conversion).