Skip to content

Commit 1a0f3ff

Browse files
committed
feat(jsoniter): remove deprecated API for cleanup and consistency
- Remove deprecated methods and interfaces, including `ContextFieldBufferMaskedPredicate`, `reset(InputStream, bufSize)`, and legacy object field probes like `applyObjField`, `applyObjFieldAsInt`, and `applyObjFieldAsLong`. - Simplify parsing entry points by fully eliminating unused/ignored parameters and redundant functionality. - Update tests and benchmarks to align with the streamlined API, ensuring backward-incompatible methods are fully replaced or removed. BREAKING CHANGE: Deprecated methods and interfaces have been removed. Consumers must migrate to the corresponding modern APIs like `applyObject`, `testObject`, or new dispatch handlers before updating.
1 parent 699726a commit 1a0f3ff

14 files changed

Lines changed: 115 additions & 446 deletions

File tree

AGENTS.md

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,22 @@ are expensive to rediscover, and that the code alone does not explain.
1717
`*BufferFunction`, `Context*`, `FieldIndex*`) that lets consumers parse without
1818
allocating per field.
1919
- `jmh/` — standalone Gradle build (`includeBuild("..")`, so it always benches local
20-
sources). `IocBench` and `MigrationBench` over two real documents. Its `README.md`
21-
holds the FieldMatcher migration decision table and the measurement methodology —
22-
that table is the justification for the current deprecations, so read it before
23-
touching the dispatch APIs.
20+
sources). `IocBench`, `MigrationBench`, and `SourceBench` over two real documents.
21+
Its `README.md` holds the FieldMatcher migration decision table and the measurement
22+
methodology — that table is the justification for the current deprecations, so read
23+
it before touching the dispatch APIs.
2424
- Releases are automated: conventional commits → release-please (`always-bump-patch`,
2525
no `v` prefix in tags) → publish. Don't hand-edit versions or `CHANGELOG.md`.
2626

2727
Build and test with `./gradlew check`. Dependencies resolve from GitHub Packages, so a
2828
`savaGithubPackagesUsername` / `savaGithubPackagesPassword` pair is needed in
29-
`~/.gradle/gradle.properties` (see `README.md`).
29+
`~/.gradle/gradle.properties` (see `README.md`). Beyond `check`, the hardening tasks
30+
(sava-build's `hardening` extension in `json-iterator/build.gradle.kts`): PIT mutation
31+
suites `pitestIterator` / `pitestNumbers` / `pitestUtil` (reports under
32+
`build/reports/pitest/<suite>/`) and the Jazzer fuzz targets described under
33+
correctness landmines. A standing task: the iterator suite's surviving/uncovered
34+
mutants are to be revisited only after the `readObject`/`readObjField` removal (see
35+
settled decisions) — most of them live in that deprecated plumbing.
3036

3137
## Correctness landmines
3238

@@ -42,8 +48,24 @@ on the chars path. A scan-path change that passes a smoke test can still be badl
4248
offset in a window rather than testing one happy path. If you touch string scanning,
4349
escape decoding, base64, or `skipUntil`, these are the tests that will catch you —
4450
and if you add a new scan path, it needs a sweep of its own.
51+
- **The heavier police are the differential fuzz targets**`fuzzJson` (byte vs char
52+
sourced iterators must produce identical event streams or both reject), `fuzzDouble`
53+
and `fuzzNumber` (JDK bit-equality oracles), `fuzzInstant`. Run the relevant one for
54+
a few minutes after any scan or parse change:
55+
`./gradlew :json-iterator:fuzzJson -PmaxFuzzTime=120`. The harness contract is
56+
strict — `JsonException` (plus `DateTimeException` for instants) is the only
57+
accepted rejection on any source, so crash-class regressions (bounds faults, stale
58+
reads past `tail`) surface as findings, not noise. When a run produces a crash
59+
input, fix the bug, then promote the input into the seed corpus
60+
(`src/test/resources/fuzz/<target>/regression-*`) so it replays forever.
4561
- Multibyte lead bytes are **negative** as signed `byte`s, and `0x80` appears
4662
mid-character in ordinary text — neither is a safe sentinel.
63+
- **Never index a lookup table with a raw source value.** A signed byte is negative on
64+
multibyte content and a char exceeds any byte-sized table, so `TABLE[c]` faults with
65+
the wrong exception class instead of rejecting. One fuzzing session found this in
66+
three separate tables (digit resolution, `VALUE_TYPES`, `JHex`); the surviving
67+
shapes are range-checked arithmetic (`BaseJsonIterator.intDigit`) or a guarded
68+
accessor (`ValueType.of`) — use those, don't add a fourth table.
4769
- `\/` is legal JSON, and `/` is in the base64 alphabet. Escape handling and base64
4870
decoding interact.
4971
- Surrogate pairs arrive as two separate `\u` escapes and must be validated as a pair.
@@ -68,6 +90,16 @@ outright removal or a silent behavior change; and workload assumptions measured
6890
local consumers (all-`byte[]` input, field-name lengths, dispatch widths) are
6991
defaults to optimize for, not invariants to depend on for correctness.
7092

93+
Removing deprecated surface follows the same doctrine, as a procedure: a
94+
`@Deprecated(forRemoval = true)` marker must ride at least one published release,
95+
and removal is cleared per member by re-running the consumer sweep — grep every
96+
jsoniter-importing `.java` file under `~/src` (skip `forks/`, build output, and dead
97+
code like idl-clients-drift), then weight each hit by its repo's last commit date.
98+
Zero hits or dormant-only hits (repos untouched for a year or more) clear removal;
99+
any actively-committed consumer blocks it until those call sites migrate. Do not
100+
trust a stale survey — the sweep is cheap, repo activity changes, and the hit list
101+
rots faster than this file is edited.
102+
71103
**`FieldMatcher` fields sit directly above the predicate that consumes them**, not at
72104
the top of the class with the other static fields. The matcher's declaration order
73105
defines the `case` indices of the switch that dispatches on it — that coupling is
@@ -80,10 +112,22 @@ full suite. The headline: `FieldMatcher` wins big on large unions (~40% at 37–
80112
and on kind/discriminator dispatch (~10% and zero allocation), but the char
81113
`fieldEquals` **chain is deliberately not deprecated** — it is the fastest option below
82114
roughly 8–10 names. Don't "modernize" small enums onto the matcher for performance;
83-
there isn't any. The deprecations (`readObject`/`readObjField`, the
84-
`testObjField`/`applyObjField*` family, the masked-buffer `testObject` overload) rest on
85-
API consistency, not on speed — `readObjField`'s String-per-field loop actually measures
86-
*faster* than the char IOC walk under ZGC.
115+
there isn't any. The deprecations rest on API consistency, not on speed —
116+
`readObjField`'s String-per-field loop actually measures *faster* than the char IOC
117+
walk under ZGC.
118+
119+
**One `forRemoval` door is still open: `readObject`/`readObjField`** (plus
120+
`JsonIterParser`'s bufSize shim, which goes in the same pass). The rest of the
121+
deprecated surface was removed in 2026-07 under the sweep procedure above; these two
122+
stayed because the sweep found five consumers with 2025–2026 commits (glam,
123+
liquid-stake-serivce, oracle_research, solscripts, rebalance-service — re-sweep
124+
before acting rather than trusting this list). Closing the door is a sequenced task,
125+
not a delete: migrate those call sites (they are single-field/config probes; the
126+
non-deprecated shapes are `applyObject`, `testObject`, or a matcher dispatch), let
127+
the deprecation ride a published release, remove the members, then re-score
128+
`pitestIterator` — the suite's uncovered-mutant mass is concentrated in exactly this
129+
plumbing, so the standing "revisit surviving mutants" task is gated behind this
130+
removal and is meaningless before it.
87131

88132
**Data source: feed `byte[]`.** Measured in `SourceBench`; the table is in
89133
`jmh/README.md`. The rule matters more than it looks, because the cost of feeding a
@@ -183,6 +227,16 @@ on a full scoreboard, but rows can be of mixed vintage. Delete an archive file t
183227
bad run's rows. Every benchmark cross-checks its variants' checksums at `@Setup`; with
184228
`-foe true` a disagreement is a hard failure, not noise.
185229

230+
**Scope the run to the change; don't default to the full suite.** A full-suite
231+
`-PjmhFork=3` run costs hours; a change-scoped `-PjmhIncludes` subset gives
232+
decision-grade rows in minutes (the 2026-07 scan-path A/B ran
233+
`SourceBench.(bytes_reset|chars_reset),IocBench.blockParse` — every touched hot path —
234+
in under five minutes per side). Pick rows by which paths the change touches:
235+
`bytes_reset`/`chars_reset` isolate the two source walks, `blockParse` adds real
236+
`readLong` volume, the dispatch rows only matter for `FieldMatcher`/chain changes.
237+
Reserve the full suite for API-shape verdicts that feed the `jmh/README.md` decision
238+
table.
239+
186240
A same-session control run matters more than a historical baseline: when judging a
187241
change, measure `main` and the change back-to-back on the same machine in the same
188242
session. Comparing against numbers from a prior day has produced wrong verdicts — the
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Benchmark (document) Mode Cnt Score Error Units
2+
IocBench.blockParse_chars N/A avgt 24 2546.089 ± 38.023 us/op
3+
IocBench.blockParse_matcher N/A avgt 24 2515.785 ± 26.194 us/op
4+
IocBench.blockParse_matcherMasked N/A avgt 24 2475.201 ± 31.146 us/op
5+
SourceBench.bytes_reset twitter avgt 24 444.823 ± 2.659 us/op
6+
SourceBench.bytes_reset solana avgt 24 3880.992 ± 28.492 us/op
7+
SourceBench.chars_reset twitter avgt 24 449.167 ± 5.704 us/op
8+
SourceBench.chars_reset solana avgt 24 4727.201 ± 61.005 us/op

0 commit comments

Comments
 (0)