Skip to content
Merged
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
48 changes: 48 additions & 0 deletions docs/src/reading.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,54 @@ json = """
employee = JSON.parse(json, Employee)
```

### Locating conversion failures

Pass `error_context=true` to identify the input value that failed while parsing:

```julia
err = try
JSON.parse("""{"counts":[1,"bad"]}""", @NamedTuple{counts::Vector{Int}};
error_context=true)
catch err
err
end

err.path # "/counts/1"
err.position # 14
err.cause isa MethodError # true
```

[`JSON.ParseError`](@ref) keeps the original exception in `cause` and its
backtrace in `backtrace`. Its `path` is an
[RFC 6901 JSON Pointer](https://www.rfc-editor.org/rfc/rfc6901): array indices
start at zero, `~` becomes `~0`, and `/` becomes `~1` within an object key.
The empty path `""` identifies the input root. Input names are used even when
field tags rename Julia fields. Duplicate keys can share a pointer; `position`
distinguishes their occurrences.

The byte position is one-based and identifies the start of the value being
converted, rather than a cursor inside a syntax error. Missing fields and
constructor failures identify the containing object. When parsing a selected
`LazyValue`, the pointer is relative to that selected value, while the position
still refers to its original buffer. JSON Lines inputs use array indices for
their lines.

This option also works with `parse!`, `parsefile`, and IO inputs. It does not
retry parsing or rerun conversion hooks to discover the path. Hooks still see
their original exceptions and can recover from them; only an error escaping
the parse call is wrapped. If a hook parses a different input or handles
multiple errors before rethrowing an older one, the enclosing conversion may
be the most precise available location. If structural path recovery fails,
`path` is `nothing` and the original cause remains available.

Diagnostics are off by default. Enabling them adds tracking to successful
conversions and a structural scan on failure. Omitting `error_context` gives the
lowest first-use cost; passing `false` disables tracking but may still add compiler work.
Existing partial updates from
`parse!` are preserved. Errors from validating options or recognizing the
initial input value, before materialization starts, keep their existing types;
interrupts, out-of-memory errors, and stack-overflow errors are never wrapped.

### Arrays and collections

You can parse JSON arrays directly into Julia arrays with a specific element type:
Expand Down
25 changes: 24 additions & 1 deletion src/JSON.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export JSONText, StructUtils, @noarg, @kwarg, @defaults, @tags, @choosetype, @no
eval(Expr(:public,
:parse, :parse!, :parsefile, :parsefile!,
:lazy, :lazyfile, :LazyValue,
:isvalidjson, :DuplicateKeyError,
:isvalidjson, :DuplicateKeyError, :ParseError,
:json, :print,
:lower, :lift,
:omit_null, :omit_empty,
Expand All @@ -38,6 +38,29 @@ function Base.showerror(io::IO, err::DuplicateKeyError)
Base.print(io, "duplicate JSON object key ", repr(err.key), " at byte position ", err.position)
end

"""
JSON.ParseError

An error encountered while constructing a value with `error_context=true`.
`path` is an RFC 6901 JSON Pointer relative to the input value, or `nothing`
if the path could not be recovered. Array indices are zero-based. `position`
is the one-based start byte of the failing value in the original input buffer.
`cause` and `backtrace` retain the original exception and its backtrace.
"""
struct ParseError <: Exception
path::Union{Nothing,String}
position::Int
cause::Any
backtrace::Any
end

function Base.showerror(io::IO, err::ParseError)
Base.print(io, "JSON parse error")
err.path === nothing || Base.print(io, " at ", repr(err.path))
Base.print(io, " (value starts at byte ", err.position, "): ")
Base.showerror(io, err.cause)
end

@enum Error InvalidJSON UnexpectedEOF ExpectedOpeningObjectChar ExpectedOpeningQuoteChar ExpectedOpeningArrayChar ExpectedClosingArrayChar ExpectedComma ExpectedColon ExpectedNewline InvalidChar InvalidNumber InvalidUTF16

@generated _typename(::Type{T}) where {T} = QuoteNode(string(T))
Expand Down
6 changes: 3 additions & 3 deletions src/lazy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,12 @@ Selectors.@selectors LazyValues
Base.lastindex(x::LazyValues) = length(x)

# this ensures LazyValues can be "sources" in StructUtils.make
function StructUtils.applyeach(::StructUtils.StructStyle, f, x::LazyValues)
function StructUtils.applyeach(st::StructUtils.StructStyle, f, x::LazyValues)
type = gettype(x)
if type == JSONTypes.OBJECT
return applyobject(f, x)
return applyobject(_contextcallback(st, f), x)
elseif type == JSONTypes.ARRAY
return applyarray(f, x)
return applyarray(_contextcallback(st, f), x)
end
typename = get(JSONTypes.names, type, "UNKNOWN")
throw(ArgumentError(string(
Expand Down
Loading