diff --git a/src/parse.jl b/src/parse.jl index 3468ea9..f98bf21 100644 --- a/src/parse.jl +++ b/src/parse.jl @@ -174,9 +174,12 @@ StructUtils.fieldtagkey(::JSONStyle) = :json StructUtils.defaultstate(st::JSONReadStyle) = StructUtils.defaultstate(st.style) # forward StructUtils API to the inner style so user-provided JSONStyle dispatches are honored -StructUtils.dictlike(st::JSONReadStyle, ::Type{T}) where {T} = StructUtils.dictlike(st.style, T) -StructUtils.arraylike(st::JSONReadStyle, ::Type{T}) where {T} = StructUtils.arraylike(st.style, T) -StructUtils.nulllike(st::JSONReadStyle, ::Type{T}) where {T} = StructUtils.nulllike(st.style, T) +StructUtils.dictlike(st::JSONReadStyle{O,N,S}, ::Type{T}) where {O,N,S<:JSONStyle,T} = + StructUtils.dictlike(st.style, T) +StructUtils.arraylike(st::JSONReadStyle{O,N,S}, ::Type{T}) where {O,N,S<:JSONStyle,T} = + StructUtils.arraylike(st.style, T) +StructUtils.nulllike(st::JSONReadStyle{O,N,S}, ::Type{T}) where {O,N,S<:JSONStyle,T} = + StructUtils.nulllike(st.style, T) # Keep structlike forwarding specific to custom JSONStyle wrappers so type-level StructStyle # specializations (for example @nonstruct types) continue to dispatch without ambiguity. StructUtils.structlike(st::JSONReadStyle{O,N,S}, ::Type{T}) where {O,N,S<:JSONStyle,T} = @@ -192,6 +195,12 @@ function jsonreadstyle(::Type{T}, ::Type{O}, null, style::StructStyle, unknown_f if T === Any && !ignore_unknown_fields throw(ArgumentError("`unknown_fields` is only supported when parsing into a target type or existing object")) end + # Avoid wrapping aggregate targets whose broad style methods would otherwise + # become ambiguous with JSONReadStyle's custom-style forwarding. + if O === DEFAULT_OBJECT_TYPE && null === nothing && ignore_unknown_fields && style isa JSONStyle && + (StructUtils.dictlike(style, T) || StructUtils.arraylike(style, T) || StructUtils.nulllike(style, T)) + return style + end return JSONReadStyle{O}(null, style, ignore_unknown_fields) end @@ -377,8 +386,12 @@ function StructUtils.make(st::StructStyle, ::Type{Any}, x::LazyValues) end # catch PtrString via lift or make! so we can ensure it never "escapes" to user-level +StructUtils.liftkey(st::JSONStyle, ::Type{T}, x::PtrString) where {T} = + StructUtils.liftkey(st, T, convert(String, x)) StructUtils.liftkey(st::JSONReadStyle, ::Type{T}, x::PtrString) where {T} = StructUtils.liftkey(st, T, convert(String, x)) +StructUtils.lift(st::JSONStyle, ::Type{T}, x::PtrString, tags) where {T} = + StructUtils.lift(st, T, convert(String, x), tags) StructUtils.lift(st::JSONReadStyle, ::Type{T}, x::PtrString, tags) where {T} = StructUtils.lift(st, T, convert(String, x), tags) StructUtils.lift(st::JSONReadStyle, ::Type{T}, x::PtrString) where {T} = @@ -386,8 +399,8 @@ StructUtils.lift(st::JSONReadStyle, ::Type{T}, x::PtrString) where {T} = # liftkey for numeric dict key types to enable round-tripping Dict{Int,V}, Dict{Float64,V}, etc. # these correspond to the lowerkey definitions in write.jl that convert numeric keys to strings -StructUtils.liftkey(::JSONReadStyle, ::Type{T}, x::AbstractString) where {T<:Integer} = Base.parse(T, x) -StructUtils.liftkey(::JSONReadStyle, ::Type{T}, x::AbstractString) where {T<:AbstractFloat} = Base.parse(T, x) +StructUtils.liftkey(::JSONStyle, ::Type{T}, x::AbstractString) where {T<:Integer} = Base.parse(T, x) +StructUtils.liftkey(::JSONStyle, ::Type{T}, x::AbstractString) where {T<:AbstractFloat} = Base.parse(T, x) _isliftpair(x) = x isa Tuple && !(x isa NamedTuple) && length(x) == 2 _liftresult(x, st) = _isliftpair(x) ? x : (x, StructUtils.defaultstate(st)) @@ -396,8 +409,10 @@ _liftresult(x, pos::Int) = _isliftpair(x) ? x : (x, pos) struct _NoCustomLazyLift end const _NO_CUSTOM_LAZY_LIFT = _NoCustomLazyLift() -StructUtils.lift(::JSONStyle, ::Type{T}, ::LazyValues, tags) where {T} = _NO_CUSTOM_LAZY_LIFT -StructUtils.lift(::JSONStyle, ::Type{T}, ::LazyValues) where {T} = _NO_CUSTOM_LAZY_LIFT +StructUtils.lift(style::JSONStyle, ::Type{T}, x::LazyValues, tags) where {T} = + _maybeliftjsonvalue(style, T, x, tags) +StructUtils.lift(style::JSONStyle, ::Type{T}, x::LazyValues) where {T} = + _maybeliftjsonvalue(style, T, x, (;)) StructUtils.lift(style::JSONReadStyle, ::Type{T}, x, tags) where {T} = _liftresult(StructUtils.lift(style.style, T, x, tags), style) @@ -420,7 +435,20 @@ function StructUtils.lift(style::JSONReadStyle, ::Type{T}, x::LazyValues) where return m, pos end -function StructUtils.lift(style::JSONReadStyle, ::Type{T}, x::LazyValues, tags=(;)) where {T} +StructUtils.lift(style::JSONReadStyle, ::Type{T}, x::LazyValues, tags) where {T} = + _liftjsonvalue(style, T, x, tags) +StructUtils.lift(style::JSONReadStyle, ::Type{T}, x::LazyValues) where {T} = + _liftjsonvalue(style, T, x, (;)) + +function _maybeliftjsonvalue(style::JSONStyle, ::Type{T}, x::LazyValues, tags) where {T} + type = gettype(x) + if type == JSONTypes.OBJECT || type == JSONTypes.ARRAY + return _NO_CUSTOM_LAZY_LIFT + end + return _liftjsonvalue(style, T, x, tags) +end + +function _liftjsonvalue(style::JSONStyle, ::Type{T}, x::LazyValues, tags) where {T} type = gettype(x) buf = getbuf(x) if type == JSONTypes.OBJECT || type == JSONTypes.ARRAY diff --git a/test/parse.jl b/test/parse.jl index a20980a..c223946 100644 --- a/test/parse.jl +++ b/test/parse.jl @@ -240,6 +240,38 @@ Base.valtype(::DictlikeViaCustomStyle) = Int StructUtils.addkeyval!(a::DictlikeViaCustomStyle, k, v) = StructUtils.addkeyval!(a.vals, k, v) StructUtils.dictlike(::CustomJSONStyle, ::Type{DictlikeViaCustomStyle}) = true +# https://github.com/JuliaIO/JSON.jl/issues/464 - broad style dispatch should not +# become ambiguous with JSONReadStyle's custom style wrapper +struct DictlikeViaStructStyle + vals::Dict{String,Int} +end +Base.keytype(::DictlikeViaStructStyle) = String +Base.valtype(::DictlikeViaStructStyle) = Int +StructUtils.initialize(::StructUtils.StructStyle, ::Type{DictlikeViaStructStyle}, source) = + DictlikeViaStructStyle(Dict{String,Int}()) +StructUtils.addkeyval!(a::DictlikeViaStructStyle, k, v) = StructUtils.addkeyval!(a.vals, k, v) +StructUtils.dictlike(::StructUtils.StructStyle, ::Type{DictlikeViaStructStyle}) = true + +struct DictlikeViaAbstractJSONStyle + vals::Dict{String,Int} +end +Base.keytype(::DictlikeViaAbstractJSONStyle) = String +Base.valtype(::DictlikeViaAbstractJSONStyle) = Int +StructUtils.initialize(::JSON.JSONStyle, ::Type{DictlikeViaAbstractJSONStyle}, source) = + DictlikeViaAbstractJSONStyle(Dict{String,Int}()) +StructUtils.addkeyval!(a::DictlikeViaAbstractJSONStyle, k, v) = StructUtils.addkeyval!(a.vals, k, v) +StructUtils.dictlike(::JSON.JSONStyle, ::Type{DictlikeViaAbstractJSONStyle}) = true + +struct DictlikeStringViaStructStyle + vals::Dict{String,String} +end +Base.keytype(::DictlikeStringViaStructStyle) = String +Base.valtype(::DictlikeStringViaStructStyle) = String +StructUtils.initialize(::StructUtils.StructStyle, ::Type{DictlikeStringViaStructStyle}, source) = + DictlikeStringViaStructStyle(Dict{String,String}()) +StructUtils.addkeyval!(a::DictlikeStringViaStructStyle, k, v) = StructUtils.addkeyval!(a.vals, k, v) +StructUtils.dictlike(::StructUtils.StructStyle, ::Type{DictlikeStringViaStructStyle}) = true + StructUtils.structlike(::RefValueStyle, ::Type{Base.RefValue{Int}}) = false StructUtils.lower(::RefValueStyle, x::Base.RefValue{Int}) = x[] StructUtils.lift(::RefValueStyle, ::Type{Base.RefValue{Int}}, x::Integer) = Ref{Int}(x), nothing @@ -800,6 +832,23 @@ JSON.lift(::DateMaterializedObjectStyle, ::Type{Date}, x::JSON.Object) = Date(x[ let res = JSON.parse("""{"a": 1, "b": 2}""", DictlikeViaCustomStyle; style=CustomJSONStyle()) @test res.vals == Dict("a" => 1, "b" => 2) end + # https://github.com/JuliaIO/JSON.jl/issues/464 - broad style-level dictlike methods must + # not conflict with JSONReadStyle's custom style wrapper + let res = JSON.parse("""{"a": 1, "b": 2}""", DictlikeViaStructStyle) + @test res.vals == Dict("a" => 1, "b" => 2) + end + let res = JSON.parse("""{"a": 1, "b": 2}""", DictlikeViaAbstractJSONStyle) + @test res.vals == Dict("a" => 1, "b" => 2) + end + let res = JSON.parse("""{"a": 1, "b": 2}""", DictlikeViaStructStyle; style=CustomJSONStyle()) + @test res.vals == Dict("a" => 1, "b" => 2) + end + let res = JSON.parse("""{"a": 1, "b": 2}""", DictlikeViaAbstractJSONStyle; style=CustomJSONStyle()) + @test res.vals == Dict("a" => 1, "b" => 2) + end + let res = JSON.parse("""{"a": "x", "b": "y"}""", DictlikeStringViaStructStyle; style=CustomJSONStyle()) + @test res.vals == Dict("a" => "x", "b" => "y") + end # https://github.com/JuliaIO/JSON.jl/issues/462 - structlike dispatch on custom JSONStyle must reach user method let json = JSON.json(Ref{Int}(1); style=RefValueStyle()) @test json == "1"