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
72 changes: 72 additions & 0 deletions .github/workflows/parsers3.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Parsers Compatibility

on:
pull_request:
workflow_dispatch:

permissions:
contents: read

env:
# Arrow -> TimeZones >=1.6 -> InlineStrings requires Parsers 2. The pinned
# Parsers 1/3 lanes therefore resolve TimeZones 1.5.9, whose build step is
# incompatible with multiple Julia thread pools (JuliaTime/TimeZones.jl#429).
JULIA_NUM_THREADS: '1'

jobs:
parsers-1:
name: Julia 1.9 - Parsers 1.1.2
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: julia-actions/setup-julia@v3
with:
version: '1.9'
- uses: julia-actions/cache@v3
- name: Test with Parsers 1
shell: julia --project=. --color=yes {0}
run: |
import Pkg
Pkg.add(Pkg.PackageSpec(name = "Parsers", version = v"1.1.2"))
Pkg.test(; coverage = true)
- uses: julia-actions/julia-processcoverage@v1
- uses: codecov/codecov-action@v7
with:
files: lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true

parsers-3:
name: Julia ${{ matrix.version }} - Parsers 3
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
version:
- '1.10'
- '1'
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v7
with:
repository: JuliaData/Parsers.jl
ref: 83c7142fb714cb87261ef38eec7ab103444eb30d
path: .ci/Parsers-v3
- uses: julia-actions/setup-julia@v3
with:
version: ${{ matrix.version }}
- uses: julia-actions/cache@v3
- name: Test with Parsers 3 PR head
env:
JSON_TEST_PARSERS_PATH: ${{ github.workspace }}/.ci/Parsers-v3
shell: julia --project=. --color=yes {0}
run: |
import Pkg
Pkg.develop(Pkg.PackageSpec(path = ENV["JSON_TEST_PARSERS_PATH"]))
Pkg.test(; coverage = true)
- uses: julia-actions/julia-processcoverage@v1
- uses: codecov/codecov-action@v7
with:
files: lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
5 changes: 3 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ JSONArrowExt = ["ArrowTypes"]
[compat]
Arrow = "2.8.0"
ArrowTypes = "2.2"
Parsers = "1, 2"
Parsers = "1, 2, 3"
PrecompileTools = "1"
StructUtils = "2.8.4"
julia = "1.9"
Expand All @@ -29,8 +29,9 @@ julia = "1.9"
Arrow = "69666777-d1a9-59fb-9406-91d4454c9d45"
ArrowTypes = "31f734f8-188a-4ce0-8406-c8a06bd891cd"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Tar = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Arrow", "Pkg", "Tar", "Test"]
test = ["Arrow", "Pkg", "Random", "Tar", "Test"]
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ JSON.json("test.json", j)
# Download json data and parse into a DataFrame
using HTTP, JSON, Tables, DataFrames
resp = HTTP.get("https://raw.githubusercontent.com/altair-viz/vega_datasets/master/vega_datasets/_data/wheat.json")
# null=missing will read json `null` as Julia `missing; `allownan=true` parses all numbers as Float64
# null=missing reads JSON `null` as Julia `missing`; allownan=true also accepts NaN and infinity
df = DataFrame(Tables.dictrowtable(JSON.parse(resp.body; null=missing, allownan=true)))
```

Expand Down
34 changes: 34 additions & 0 deletions docs/src/migrate.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,40 @@ This guide provides an overview of how to migrate your code from either the pre-

---

## Number parsing with Parsers 3

JSON.jl accepts Parsers 1, 2, and 3. Parsers 3 requires Julia 1.10 or later;
Julia 1.9 continues to use the compatibility path. This dependency update does
not change standards-compliant JSON syntax or the four adaptive untyped result
types.

There are six observable corrections:

- `typemin(Int64)` now materializes as `Int64` instead of `BigInt`.
- `allownan=true` only enables configured and native special-value spellings.
It no longer forces finite integers through `Float64`.
- A leading `+` on a finite number is rejected. This removes a nonstandard
extension; `+Inf` remains available behind `allownan=true`.
- Negative zero keeps its sign, including extreme exponent forms such as
`-0e291` that lost the sign on the Parsers 2 compatibility path.
- Long finite decimals now use correct Base-compatible rounding. This also
avoids rare Parsers 1/2 failures and wrong results on valid JSON numbers.
- Supported built-in typed numeric targets use the original token bytes with
Parsers 3. This avoids intermediate `Float64` rounding for `Float32` and
`BigFloat`. Custom `StructUtils` styles and field lifts keep the prior
adaptive conversion path.

On the Parsers 1 and 2 compatibility paths, JSON gives validated finite float
and overflowed integer spans to Base. A byte-vector input therefore needs a
temporary `String` for these legacy dependency versions. Parsers 3 converts the
original string or byte span directly and does not have this compatibility cost.

Code that depended on `allownan=true` to coerce every finite number to
`Float64` should request a typed target instead, for example
`JSON.parse(source, Vector{Float64}; allownan=true)`.

---

## Migration guide from pre-1.0 -> 1.0

### Writing JSON
Expand Down
17 changes: 15 additions & 2 deletions docs/src/reading.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ The core JSON parsing machinery is hence built around having an `AbstractVector{

Each entrypoint function first calls [`JSON.lazy`](@ref), which will consume the JSON input until the type of the next JSON value can be identified (`{` for objects, `[` for arrays, `"` for strings, `t` for true, `f` for false, `n` for null, and `-` or a digit for numbers). [`JSON.lazy`](@ref) returns a [`JSON.LazyValue`](@ref), which wraps the JSON input buffer (`AbstractVector{UInt8}` or `AbstractString`), and marks the byte position the value starts at, the type of the value, and any keyword arguments that were provided that may affect parsing. Currently supported parsing-specific keyword arguments to [`JSON.lazy`](@ref) (and thus all other entrypoint functions) include:

- `allownan::Bool = false`: whether "special" float values shoudl be allowed while parsing (`NaN`, `Inf`, `-Inf`); these values are specifically _not allowed_ in the JSON spec, but many JSON libraries allow reading/writing
- `allownan::Bool = false`: whether "special" float values should be allowed while parsing (`NaN`, `Inf`, `-Inf`); these values are specifically _not allowed_ in the JSON spec, but many JSON libraries allow reading/writing. Finite numbers keep their normal adaptive type selection.
- `ninf::String = "-Infinity"`: the string that will be used to parse `-Inf` if `allownan=true`
- `inf::String = "Infinity"`: the string that will be used to parse `Inf` if `allownan=true`
- `nan::String = "NaN"`: the string that will be sued to parse `NaN` if `allownan=true`
- `nan::String = "NaN"`: the string that will be used to parse `NaN` if `allownan=true`
- `jsonlines::Bool = false`: whether the JSON input should be treated as an implicit array, with newlines separating individual JSON elements with no leading `'['` or trailing `']'` characters. Common in logging or streaming workflows. Defaults to `true` when used with [`JSON.parsefile`](@ref) and the filename extension is `.jsonl` or `ndjson`. Note this ensures that parsing will _always_ return an array at the root-level.
- Materialization-specific keyword arguments (i.e. they affect materialization, but not parsing)
- `dicttype = JSON.Object{String, Any}`: type to parse JSON objects as by default (recursively)
Expand Down Expand Up @@ -135,6 +135,12 @@ Under the hood, this `getindex` call is really calling `JSON.parse(lazyvalue)`.
| `null` | `nothing` |
| `true/false` | `Bool` |

Finite integers use `Int64` when possible, including `typemin(Int64)`, and
promote to `BigInt` outside that range. Finite decimal or exponent forms use
`Float64` when representable and promote overflowed values to `BigFloat`.
Setting `allownan=true` only adds special-value spellings; it does not force
finite values through `Float64`.

Mostly vanilla, but what is `JSON.Object`? It is a custom `AbstractDict` using an internal linked-list implementation that preserves insertion order, behaves as a drop-in replacement for `Dict`, and allows memory and performance benefits vs. `Dict` for small # of entries. It also supports natural JSON-object-like
syntax for accessing or setting values, like `x.g.h.i` and `x.c = false`.

Expand Down Expand Up @@ -255,6 +261,13 @@ date = JSON.parse("\"2023-05-08\"", Date)
# Date("2023-05-08")
```

With Parsers 3 on Julia 1.10 or later, supported built-in numeric targets are
converted from the original JSON token bytes. In particular, `Float32` and
`BigFloat` do not pass through an intermediate `Float64`. Numeric fields with
a custom `StructUtils` style or field tag keep the adaptive value-and-lift
path, so existing custom conversions continue to receive `Int64`, `BigInt`,
`Float64`, or `BigFloat` values.

### Type conversions and handling nulls

JSON.jl provides smart handling for Union types, especially for dealing with potentially null values:
Expand Down
Loading
Loading