Deserealize from/to JSON

I would like to have a quick and dirty way to load/save custom julia structs to a human readable format, say JSON.

Goals

  • Human readable
  • Works out of the box for user defined structs
  • Should be simple to implement
  • Should not require passing type information

Non Goals

  • Performance
  • Security

Here is what I tried so far:

module H
using ArgCheck
using ConstructionBase: constructorof

KEY_META = "__meta__"
KEY_CONSTRUCTOR = "constructor"
KEY_PROPERTYNAMES = "propertynames"

function marshal_struct(o)
    d = Dict{String, Any}()
    meta = Dict(
        KEY_CONSTRUCTOR => string(constructorof(typeof(o))),
        KEY_PROPERTYNAMES => collect(map(string, propertynames(o))),
    )
    d[KEY_META] = meta
    for prop in propertynames(o)
        key = string(prop)
        d[key] = marshal(getproperty(o, prop))
    end
    d
end

marshal(o) = marshal_struct(o)
marshal(o::String) = o
marshal(o::Number) = o

unmarshal(o::String, ctx) = o
unmarshal(o::Number, ctx) = o

function unmarshal(d::Dict, eval)
    @argcheck haskey(d, KEY_META)
    meta = d[KEY_META]
    @check haskey(meta, KEY_CONSTRUCTOR)
    @check haskey(meta, KEY_PROPERTYNAMES)
    
    ctor_str = meta[KEY_CONSTRUCTOR]
    @check ctor_str isa String
    ctor_expr = nothing
    try
        ctor_expr = Meta.parse(ctor_str)
    catch err
        msg = """Error parsing constructor:
        string: $ctor_str
        error: $err
        """
        error(msg)
    end
    ctor = nothing
    try
        ctor = eval(ctor_expr)
    catch err
        msg = """Error evaluating constructor expression:
        string: $ctor_str
        expr: $ctor_expr
        error: $err
        """
        error(msg)
    end
    propnames = meta[KEY_PROPERTYNAMES]
    args = map(propnames) do key
        unmarshal(d[key], eval)
    end
    ctor(args...)
end
end#module

struct S{A,B}
    a::A
    b::B
end

s = S(1, 2.0)
d = H.marshal(s)
s2 = H.unmarshal(d, eval)
@show s
@show d
@show s2
s = S{Int64,Float64}(1, 2.0)
d = Dict{String,Any}("b" => 2.0,"__meta__" => Dict{String,Any}("propertynames" => ["a", "b"],"constructor" => "S"),"a" => 1)
s2 = S{Int64,Float64}(1, 2.0)

In order to load user defined structs, one needs to call eval at some point. My problem is, that this eval must be the eval of the calling module, not the eval of the module that defines the marshaling functionality. That is why the user needs to pass his eval

How to avoid passing the user module eval?
JLD.jl must have solved this problem, but I cannot understand the code.
Or is there another approach?

It seems that JLD.jl uses Core.eval(Main, expr) for evaluating the type.