Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 36 additions & 8 deletions src/parse.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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} =
Expand All @@ -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

Expand Down Expand Up @@ -377,17 +386,21 @@ 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} =
StructUtils.lift(st, T, convert(String, x))

# 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))
Expand All @@ -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)
Expand All @@ -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
Expand Down
49 changes: 49 additions & 0 deletions test/parse.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
Loading