JSON3 and Tuples

I am having difficulty using JSON3 and a dictionary with Tuple keys.
The first example works as expected the second does not. I have tried to use StructTypes to fix this, but I am missing something.

dString=Dict("p1"=>1,"p2"=>1,"p3"=>2,"p4"=>3)
@show buffS=JSON3.write(dString)
rString=JSON3.read(buffS,Dict{String, Int64})

produces

buffS = JSON3.write(dString) = "{\"p4\":3,\"p3\":2,\"p2\":1,\"p1\":1}"
Dict{String, Int64} with 4 entries:
  "p4" => 3
  "p3" => 2
  "p2" => 1
  "p1" => 1

but

DT=Dict{Tuple{Vararg{String}}, Int64}
@show dTuple=DT(("p1","c2")=>1,("p2","c2")=>1,("p3","c7","b100")=>2)
@show buffT=JSON3.write(dTuple)
rTuple=JSON3.read(buffS,DT)

produces

dTuple = DT(("p1", "c2") => 1, ("p2", "c2") => 1, ("p3", "c7", "b100") => 2) = Dict{Tuple{Vararg{String, N} where N}, Int64}(("p2", "c2") => 1, ("p1", "c2") => 1, ("p3", "c7", "b100") => 2)
buffT = JSON3.write(dTuple) = "{\"(\\\"p2\\\", \\\"c2\\\")\":1,\"(\\\"p1\\\", \\\"c2\\\")\":1,\"(\\\"p3\\\", \\\"c7\\\", \\\"b100\\\")\":2}"
MethodError: Cannot `convert` an object of type Char to an object of type String
Closest candidates are:
  convert(::Type{String}, ::String) at essentials.jl:210
  convert(::Type{T}, ::T) where T<:AbstractString at strings/basic.jl:231
  convert(::Type{T}, ::AbstractString) where T<:AbstractString at strings/basic.jl:232
  ...

Stacktrace:
  [1] setindex!(A::Vector{String}, x::Char, i1::Int64)
    @ Base ./array.jl:839
  [2] copyto!(dest::Vector{String}, src::String)
    @ Base ./abstractarray.jl:846
  [3] _collect
    @ ./array.jl:563 [inlined]
  [4] collect
    @ ./array.jl:561 [inlined]
  [5] _totuple
    @ ./tuple.jl:329 [inlined]
  [6] Tuple
    @ ./tuple.jl:303 [inlined]
  [7] #construct#1
    @ ~/.julia/packages/StructTypes/NJXhA/src/StructTypes.jl:310 [inlined]
  [8] construct
    @ ~/.julia/packages/StructTypes/NJXhA/src/StructTypes.jl:310 [inlined]
  [9] keyvalue
    @ ~/.julia/packages/JSON3/PgKj8/src/structs.jl:282 [inlined]
 [10] #read#36
    @ ~/.julia/packages/JSON3/PgKj8/src/structs.jl:328 [inlined]
 [11] read
    @ ~/.julia/packages/JSON3/PgKj8/src/structs.jl:291 [inlined]
 [12] #read#35
    @ ~/.julia/packages/JSON3/PgKj8/src/structs.jl:288 [inlined]
 [13] read
    @ ~/.julia/packages/JSON3/PgKj8/src/structs.jl:288 [inlined]
 [14] read(str::String, ::Type{Dict{Tuple{Vararg{String, N} where N}, Int64}}; kw::Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
    @ JSON3 ~/.julia/packages/JSON3/PgKj8/src/structs.jl:34
 [15] read(str::String, ::Type{Dict{Tuple{Vararg{String, N} where N}, Int64}})
    @ JSON3 ~/.julia/packages/JSON3/PgKj8/src/structs.jl:33
 [16] top-level scope
    @ In[125]:4
 [17] eval
    @ ./boot.jl:360 [inlined]
 [18] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String)
    @ Base ./loading.jl:1094

The JSON format only supports string keys, so that won’t work. If you want to use JSON, you can use an array of [key, value] arrays.

Thanks. That explains it.

I actually have this dictionary as part of a struct (in code someone else wrote so can’t modify that). Is there some way to teach JSON3 to convert this to a [key, value] array and then when it reads it back in convert it to a dictionary. I could do this by hand, but it would be nice if it did this automagically.

Thanks for the help again. I am new to using JSON

I think StructTypes.jl can do that kind of thing (example) but I haven’t done it myself.

I think it’d be easier for you to define the parent struct as a CustomStruct, which then allows you to define StructTypes.lower(x::MyStruct) = ... to change your struct into another object with the representation you want. So for example, if your struct is defined like:

struct Foo
    id::Int
    vals::Dict{String, Int}
end

Then you do the StructTypes definitions like:

StructTypes.StructType(::Type{Foo}) = StructTypes.CustomStruct()
StructTypes.lower(x::Foo) = (id=x.id, vals=[k => v for (k, v) in x.vals])

This changes vals to be serialized as an array of key-value pairs, instead of as an object, which is the default for Dict.

1 Like

This is fantastic. Thank you. It worked just as advertised.
Now I am trying to read the object back in and get the previous dictionary indexed by Tuples.

struct Foo2
    id::Int
    vals::Dict{Tuple{Vararg{String}}, Int64}
end

StructTypes.StructType(::Type{Foo2}) = StructTypes.CustomStruct()
StructTypes.lower(x::Foo2) = (id=x.id, vals=[k => v for (k, v) in x.vals])

buff=JSON3.write(foo2)
rd=JSON3.read(buff,Foo2)

Which produces the following error. I understand that I need to make a constructor for the type Foo2. But I have failed at all attempts so far.

MethodError: no method matching Foo2(::Dict{String, Any})
Closest candidates are:
  Foo2(::Any, ::Any) at In[9]:2
  Foo2(::Int64, ::Dict{Tuple{Vararg{String, N} where N}, Int64}) at In[9]:2

Stacktrace:
 [1] construct(T::Type, args::Dict{String, Any}; kw::Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
   @ StructTypes ~/.julia/packages/StructTypes/NJXhA/src/StructTypes.jl:310
 [2] construct(T::Type, args::Dict{String, Any})
   @ StructTypes ~/.julia/packages/StructTypes/NJXhA/src/StructTypes.jl:310
 [3] #read#37
   @ ~/.julia/packages/JSON3/PgKj8/src/structs.jl:372 [inlined]
 [4] read
   @ ~/.julia/packages/JSON3/PgKj8/src/structs.jl:370 [inlined]
 [5] read(str::String, ::Type{Foo2}; kw::Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
   @ JSON3 ~/.julia/packages/JSON3/PgKj8/src/structs.jl:34
 [6] read(str::String, ::Type{Foo2})
   @ JSON3 ~/.julia/packages/JSON3/PgKj8/src/structs.jl:33
 [7] top-level scope
   @ In[9]:10
 [8] eval
   @ ./boot.jl:360 [inlined]
 [9] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String)
   @ Base ./loading.jl:1094

So the other piece mentioned in the CustomStruct docs, is that you probably want to define a StructTypes.lowertype method, so deserialization gives you something better when deserialization; something like:

StructTypes.lowertype(::Type{Foo2}) = NamedTuple{(:id, :vals), Tuple{Int, Vector{Any}}}

then you’ll need a constructor with a signature like:

function Foo2(id::Int, vals::Vector)
    # convert vals from Vector{Any} to Dict
end
1 Like