Skip to content

Commit 064045e

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 6e487a9 commit 064045e

2 files changed

Lines changed: 80 additions & 8 deletions

File tree

src/parse.jl

Lines changed: 44 additions & 8 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,12 @@ 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+
# Avoid wrapping aggregate targets whose broad style methods would otherwise
199+
# become ambiguous with JSONReadStyle's custom-style forwarding.
200+
if O === DEFAULT_OBJECT_TYPE && null === nothing && ignore_unknown_fields && style isa JSONStyle &&
201+
(StructUtils.dictlike(style, T) || StructUtils.arraylike(style, T) || StructUtils.nulllike(style, T))
202+
return style
203+
end
195204
return JSONReadStyle{O}(null, style, ignore_unknown_fields)
196205
end
197206

@@ -377,17 +386,23 @@ function StructUtils.make(st::StructStyle, ::Type{Any}, x::LazyValues)
377386
end
378387

379388
# catch PtrString via lift or make! so we can ensure it never "escapes" to user-level
389+
StructUtils.liftkey(st::JSONStyle, ::Type{T}, x::PtrString) where {T} =
390+
StructUtils.liftkey(st, T, convert(String, x))
380391
StructUtils.liftkey(st::JSONReadStyle, ::Type{T}, x::PtrString) where {T} =
381392
StructUtils.liftkey(st, T, convert(String, x))
393+
StructUtils.lift(st::JSONStyle, ::Type{T}, x::PtrString, tags) where {T} =
394+
StructUtils.lift(st, T, convert(String, x), tags)
382395
StructUtils.lift(st::JSONReadStyle, ::Type{T}, x::PtrString, tags) where {T} =
383396
StructUtils.lift(st, T, convert(String, x), tags)
397+
StructUtils.lift(st::JSONStyle, ::Type{T}, x::PtrString) where {T} =
398+
StructUtils.lift(st, T, convert(String, x))
384399
StructUtils.lift(st::JSONReadStyle, ::Type{T}, x::PtrString) where {T} =
385400
StructUtils.lift(st, T, convert(String, x))
386401

387402
# liftkey for numeric dict key types to enable round-tripping Dict{Int,V}, Dict{Float64,V}, etc.
388403
# 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)
404+
StructUtils.liftkey(::JSONStyle, ::Type{T}, x::AbstractString) where {T<:Integer} = Base.parse(T, x)
405+
StructUtils.liftkey(::JSONStyle, ::Type{T}, x::AbstractString) where {T<:AbstractFloat} = Base.parse(T, x)
391406

392407
_isliftpair(x) = x isa Tuple && !(x isa NamedTuple) && length(x) == 2
393408
_liftresult(x, st) = _isliftpair(x) ? x : (x, StructUtils.defaultstate(st))
@@ -396,8 +411,10 @@ _liftresult(x, pos::Int) = _isliftpair(x) ? x : (x, pos)
396411
struct _NoCustomLazyLift end
397412
const _NO_CUSTOM_LAZY_LIFT = _NoCustomLazyLift()
398413

399-
StructUtils.lift(::JSONStyle, ::Type{T}, ::LazyValues, tags) where {T} = _NO_CUSTOM_LAZY_LIFT
400-
StructUtils.lift(::JSONStyle, ::Type{T}, ::LazyValues) where {T} = _NO_CUSTOM_LAZY_LIFT
414+
StructUtils.lift(style::JSONStyle, ::Type{T}, x::LazyValues, tags) where {T} =
415+
_maybeliftjsonvalue(style, T, x, tags)
416+
StructUtils.lift(style::JSONStyle, ::Type{T}, x::LazyValues) where {T} =
417+
_maybeliftjsonvalue(style, T, x, (;))
401418

402419
StructUtils.lift(style::JSONReadStyle, ::Type{T}, x, tags) where {T} =
403420
_liftresult(StructUtils.lift(style.style, T, x, tags), style)
@@ -414,13 +431,32 @@ function customlazylift(style::JSONReadStyle, ::Type{T}, x::LazyValues, tags) wh
414431
return nothing
415432
end
416433

434+
function StructUtils.lift(style::JSONStyle, ::Type{T}, x::LazyValues) where {T<:AbstractArray{E,0}} where {E}
435+
m = T(undef)
436+
m[1], pos = StructUtils.lift(style, E, x)
437+
return m, pos
438+
end
439+
417440
function StructUtils.lift(style::JSONReadStyle, ::Type{T}, x::LazyValues) where {T<:AbstractArray{E,0}} where {E}
418441
m = T(undef)
419442
m[1], pos = StructUtils.lift(style, E, x)
420443
return m, pos
421444
end
422445

423-
function StructUtils.lift(style::JSONReadStyle, ::Type{T}, x::LazyValues, tags=(;)) where {T}
446+
StructUtils.lift(style::JSONReadStyle, ::Type{T}, x::LazyValues, tags) where {T} =
447+
_liftjsonvalue(style, T, x, tags)
448+
StructUtils.lift(style::JSONReadStyle, ::Type{T}, x::LazyValues) where {T} =
449+
_liftjsonvalue(style, T, x, (;))
450+
451+
function _maybeliftjsonvalue(style::JSONStyle, ::Type{T}, x::LazyValues, tags) where {T}
452+
type = gettype(x)
453+
if type == JSONTypes.OBJECT || type == JSONTypes.ARRAY
454+
return _NO_CUSTOM_LAZY_LIFT
455+
end
456+
return _liftjsonvalue(style, T, x, tags)
457+
end
458+
459+
function _liftjsonvalue(style::JSONStyle, ::Type{T}, x::LazyValues, tags) where {T}
424460
type = gettype(x)
425461
buf = getbuf(x)
426462
if type == JSONTypes.OBJECT || type == JSONTypes.ARRAY

test/parse.jl

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

243+
# https://github.com/JuliaIO/JSON.jl/issues/464 - broad style dispatch should not
244+
# become ambiguous with JSONReadStyle's custom style wrapper
245+
struct DictlikeViaStructStyle
246+
vals::Dict{String,Int}
247+
end
248+
Base.keytype(::DictlikeViaStructStyle) = String
249+
Base.valtype(::DictlikeViaStructStyle) = Int
250+
StructUtils.initialize(::StructUtils.StructStyle, ::Type{DictlikeViaStructStyle}, source) =
251+
DictlikeViaStructStyle(Dict{String,Int}())
252+
StructUtils.addkeyval!(a::DictlikeViaStructStyle, k, v) = StructUtils.addkeyval!(a.vals, k, v)
253+
StructUtils.dictlike(::StructUtils.StructStyle, ::Type{DictlikeViaStructStyle}) = true
254+
255+
struct DictlikeViaAbstractJSONStyle
256+
vals::Dict{String,Int}
257+
end
258+
Base.keytype(::DictlikeViaAbstractJSONStyle) = String
259+
Base.valtype(::DictlikeViaAbstractJSONStyle) = Int
260+
StructUtils.initialize(::JSON.JSONStyle, ::Type{DictlikeViaAbstractJSONStyle}, source) =
261+
DictlikeViaAbstractJSONStyle(Dict{String,Int}())
262+
StructUtils.addkeyval!(a::DictlikeViaAbstractJSONStyle, k, v) = StructUtils.addkeyval!(a.vals, k, v)
263+
StructUtils.dictlike(::JSON.JSONStyle, ::Type{DictlikeViaAbstractJSONStyle}) = true
264+
243265
StructUtils.structlike(::RefValueStyle, ::Type{Base.RefValue{Int}}) = false
244266
StructUtils.lower(::RefValueStyle, x::Base.RefValue{Int}) = x[]
245267
StructUtils.lift(::RefValueStyle, ::Type{Base.RefValue{Int}}, x::Integer) = Ref{Int}(x), nothing
@@ -800,6 +822,20 @@ JSON.lift(::DateMaterializedObjectStyle, ::Type{Date}, x::JSON.Object) = Date(x[
800822
let res = JSON.parse("""{"a": 1, "b": 2}""", DictlikeViaCustomStyle; style=CustomJSONStyle())
801823
@test res.vals == Dict("a" => 1, "b" => 2)
802824
end
825+
# https://github.com/JuliaIO/JSON.jl/issues/464 - broad style-level dictlike methods must
826+
# not conflict with JSONReadStyle's custom style wrapper
827+
let res = JSON.parse("""{"a": 1, "b": 2}""", DictlikeViaStructStyle)
828+
@test res.vals == Dict("a" => 1, "b" => 2)
829+
end
830+
let res = JSON.parse("""{"a": 1, "b": 2}""", DictlikeViaAbstractJSONStyle)
831+
@test res.vals == Dict("a" => 1, "b" => 2)
832+
end
833+
let res = JSON.parse("""{"a": 1, "b": 2}""", DictlikeViaStructStyle; style=CustomJSONStyle())
834+
@test res.vals == Dict("a" => 1, "b" => 2)
835+
end
836+
let res = JSON.parse("""{"a": 1, "b": 2}""", DictlikeViaAbstractJSONStyle; style=CustomJSONStyle())
837+
@test res.vals == Dict("a" => 1, "b" => 2)
838+
end
803839
# https://github.com/JuliaIO/JSON.jl/issues/462 - structlike dispatch on custom JSONStyle must reach user method
804840
let json = JSON.json(Ref{Int}(1); style=RefValueStyle())
805841
@test json == "1"

0 commit comments

Comments
 (0)