Skip to content

Commit cc0619d

Browse files
committed
fix(parse): avoid style wrapper ambiguity
Avoid wrapping custom JSONStyle values on the default parse path, while keeping JSON lazy-value lifting available on JSONStyle itself. This lets broad style-level StructUtils methods dispatch without conflicting with JSONReadStyle forwarding. Fixes #464
1 parent 9310c58 commit cc0619d

2 files changed

Lines changed: 69 additions & 6 deletions

File tree

src/parse.jl

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,12 @@ StructUtils.fieldtagkey(::JSONStyle) = :json
174174
StructUtils.defaultstate(st::JSONReadStyle) = StructUtils.defaultstate(st.style)
175175

176176
# forward StructUtils API to the inner style so user-provided JSONStyle dispatches are honored
177-
StructUtils.dictlike(st::JSONReadStyle, ::Type{T}) where {T} = StructUtils.dictlike(st.style, T)
178-
StructUtils.arraylike(st::JSONReadStyle, ::Type{T}) where {T} = StructUtils.arraylike(st.style, T)
179-
StructUtils.nulllike(st::JSONReadStyle, ::Type{T}) where {T} = StructUtils.nulllike(st.style, T)
177+
StructUtils.dictlike(st::JSONReadStyle{O,N,S}, ::Type{T}) where {O,N,S<:JSONStyle,T} =
178+
StructUtils.dictlike(st.style, T)
179+
StructUtils.arraylike(st::JSONReadStyle{O,N,S}, ::Type{T}) where {O,N,S<:JSONStyle,T} =
180+
StructUtils.arraylike(st.style, T)
181+
StructUtils.nulllike(st::JSONReadStyle{O,N,S}, ::Type{T}) where {O,N,S<:JSONStyle,T} =
182+
StructUtils.nulllike(st.style, T)
180183
# Keep structlike forwarding specific to custom JSONStyle wrappers so type-level StructStyle
181184
# specializations (for example @nonstruct types) continue to dispatch without ambiguity.
182185
StructUtils.structlike(st::JSONReadStyle{O,N,S}, ::Type{T}) where {O,N,S<:JSONStyle,T} =
@@ -192,6 +195,11 @@ function jsonreadstyle(::Type{T}, ::Type{O}, null, style::StructStyle, unknown_f
192195
if T === Any && !ignore_unknown_fields
193196
throw(ArgumentError("`unknown_fields` is only supported when parsing into a target type or existing object"))
194197
end
198+
# A JSONStyle already has JSON field tags and lazy-value lifting; avoid wrapping
199+
# the default path so broad style methods do not become ambiguous.
200+
if O === DEFAULT_OBJECT_TYPE && null === nothing && ignore_unknown_fields && style isa JSONStyle
201+
return style
202+
end
195203
return JSONReadStyle{O}(null, style, ignore_unknown_fields)
196204
end
197205

@@ -377,28 +385,47 @@ function StructUtils.make(st::StructStyle, ::Type{Any}, x::LazyValues)
377385
end
378386

379387
# catch PtrString via lift or make! so we can ensure it never "escapes" to user-level
388+
StructUtils.liftkey(st::JSONStyle, ::Type{T}, x::PtrString) where {T} =
389+
StructUtils.liftkey(st, T, convert(String, x))
380390
StructUtils.liftkey(st::JSONReadStyle, ::Type{T}, x::PtrString) where {T} =
381391
StructUtils.liftkey(st, T, convert(String, x))
392+
StructUtils.lift(st::JSONStyle, ::Type{T}, x::PtrString, tags) where {T} =
393+
StructUtils.lift(st, T, convert(String, x), tags)
382394
StructUtils.lift(st::JSONReadStyle, ::Type{T}, x::PtrString, tags) where {T} =
383395
StructUtils.lift(st, T, convert(String, x), tags)
396+
StructUtils.lift(st::JSONStyle, ::Type{T}, x::PtrString) where {T} =
397+
StructUtils.lift(st, T, convert(String, x))
384398
StructUtils.lift(st::JSONReadStyle, ::Type{T}, x::PtrString) where {T} =
385399
StructUtils.lift(st, T, convert(String, x))
386400

387401
# liftkey for numeric dict key types to enable round-tripping Dict{Int,V}, Dict{Float64,V}, etc.
388402
# these correspond to the lowerkey definitions in write.jl that convert numeric keys to strings
389-
StructUtils.liftkey(::JSONReadStyle, ::Type{T}, x::AbstractString) where {T<:Integer} = Base.parse(T, x)
390-
StructUtils.liftkey(::JSONReadStyle, ::Type{T}, x::AbstractString) where {T<:AbstractFloat} = Base.parse(T, x)
403+
StructUtils.liftkey(::JSONStyle, ::Type{T}, x::AbstractString) where {T<:Integer} = Base.parse(T, x)
404+
StructUtils.liftkey(::JSONStyle, ::Type{T}, x::AbstractString) where {T<:AbstractFloat} = Base.parse(T, x)
391405

392406
StructUtils.lift(style::JSONReadStyle, ::Type{T}, x, tags) where {T} = StructUtils.lift(style.style, T, x, tags)
393407
StructUtils.lift(style::JSONReadStyle, ::Type{T}, x) where {T} = StructUtils.lift(style.style, T, x)
394408

409+
function StructUtils.lift(style::JSONStyle, ::Type{T}, x::LazyValues) where {T<:AbstractArray{E,0}} where {E}
410+
m = T(undef)
411+
m[1], pos = StructUtils.lift(style, E, x)
412+
return m, pos
413+
end
414+
395415
function StructUtils.lift(style::JSONReadStyle, ::Type{T}, x::LazyValues) where {T<:AbstractArray{E,0}} where {E}
396416
m = T(undef)
397417
m[1], pos = StructUtils.lift(style, E, x)
398418
return m, pos
399419
end
400420

401-
function StructUtils.lift(style::JSONReadStyle, ::Type{T}, x::LazyValues, tags=(;)) where {T}
421+
StructUtils.lift(style::JSONStyle, ::Type{T}, x::LazyValues, tags=(;)) where {T} =
422+
_liftjsonvalue(style, T, x, tags)
423+
StructUtils.lift(style::JSONReadStyle, ::Type{T}, x::LazyValues, tags) where {T} =
424+
_liftjsonvalue(style, T, x, tags)
425+
StructUtils.lift(style::JSONReadStyle, ::Type{T}, x::LazyValues) where {T} =
426+
_liftjsonvalue(style, T, x, (;))
427+
428+
function _liftjsonvalue(style::JSONStyle, ::Type{T}, x::LazyValues, tags) where {T}
402429
type = gettype(x)
403430
buf = getbuf(x)
404431
if type == JSONTypes.STRING

test/parse.jl

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,28 @@ Base.valtype(::DictlikeViaCustomStyle) = Int
237237
StructUtils.addkeyval!(a::DictlikeViaCustomStyle, k, v) = StructUtils.addkeyval!(a.vals, k, v)
238238
StructUtils.dictlike(::CustomJSONStyle, ::Type{DictlikeViaCustomStyle}) = true
239239

240+
# https://github.com/JuliaIO/JSON.jl/issues/464 - broad style dispatch should not
241+
# become ambiguous with JSONReadStyle's custom style wrapper
242+
struct DictlikeViaStructStyle
243+
vals::Dict{String,Int}
244+
end
245+
Base.keytype(::DictlikeViaStructStyle) = String
246+
Base.valtype(::DictlikeViaStructStyle) = Int
247+
StructUtils.initialize(::StructUtils.StructStyle, ::Type{DictlikeViaStructStyle}, source) =
248+
DictlikeViaStructStyle(Dict{String,Int}())
249+
StructUtils.addkeyval!(a::DictlikeViaStructStyle, k, v) = StructUtils.addkeyval!(a.vals, k, v)
250+
StructUtils.dictlike(::StructUtils.StructStyle, ::Type{DictlikeViaStructStyle}) = true
251+
252+
struct DictlikeViaAbstractJSONStyle
253+
vals::Dict{String,Int}
254+
end
255+
Base.keytype(::DictlikeViaAbstractJSONStyle) = String
256+
Base.valtype(::DictlikeViaAbstractJSONStyle) = Int
257+
StructUtils.initialize(::JSON.JSONStyle, ::Type{DictlikeViaAbstractJSONStyle}, source) =
258+
DictlikeViaAbstractJSONStyle(Dict{String,Int}())
259+
StructUtils.addkeyval!(a::DictlikeViaAbstractJSONStyle, k, v) = StructUtils.addkeyval!(a.vals, k, v)
260+
StructUtils.dictlike(::JSON.JSONStyle, ::Type{DictlikeViaAbstractJSONStyle}) = true
261+
240262
StructUtils.structlike(::RefValueStyle, ::Type{Base.RefValue{Int}}) = false
241263
StructUtils.lower(::RefValueStyle, x::Base.RefValue{Int}) = x[]
242264
StructUtils.lift(::RefValueStyle, ::Type{Base.RefValue{Int}}, x::Integer) = Ref{Int}(x), nothing
@@ -786,6 +808,20 @@ StructUtils.lift(::RefValueStyle, ::Type{Base.RefValue{Int}}, x::Integer) = Ref{
786808
let res = JSON.parse("""{"a": 1, "b": 2}""", DictlikeViaCustomStyle; style=CustomJSONStyle())
787809
@test res.vals == Dict("a" => 1, "b" => 2)
788810
end
811+
# https://github.com/JuliaIO/JSON.jl/issues/464 - broad style-level dictlike methods must
812+
# not conflict with JSONReadStyle's custom style wrapper
813+
let res = JSON.parse("""{"a": 1, "b": 2}""", DictlikeViaStructStyle)
814+
@test res.vals == Dict("a" => 1, "b" => 2)
815+
end
816+
let res = JSON.parse("""{"a": 1, "b": 2}""", DictlikeViaAbstractJSONStyle)
817+
@test res.vals == Dict("a" => 1, "b" => 2)
818+
end
819+
let res = JSON.parse("""{"a": 1, "b": 2}""", DictlikeViaStructStyle; style=CustomJSONStyle())
820+
@test res.vals == Dict("a" => 1, "b" => 2)
821+
end
822+
let res = JSON.parse("""{"a": 1, "b": 2}""", DictlikeViaAbstractJSONStyle; style=CustomJSONStyle())
823+
@test res.vals == Dict("a" => 1, "b" => 2)
824+
end
789825
# https://github.com/JuliaIO/JSON.jl/issues/462 - structlike dispatch on custom JSONStyle must reach user method
790826
let json = JSON.json(Ref{Int}(1); style=RefValueStyle())
791827
@test json == "1"

0 commit comments

Comments
 (0)