Skip to content

feat(java): add token-level readUtf8/writeUtf8 operations to ForyJson - #4180

Draft
dstepanov wants to merge 3 commits into
apache:mainfrom
dstepanov:json-reader-writer-factories
Draft

dstepanov wants to merge 3 commits into
apache:mainfrom
dstepanov:json-reader-writer-factories

Conversation

@dstepanov

@dstepanov dstepanov commented Oct 14, 2026 •

Copy link
Copy Markdown

Why?

Micronaut is a JVM framework built around a compile-time-first approach: dependency injection, AOP, bean introspection and configuration are resolved by annotation processors at build time instead of with runtime reflection, proxies or bytecode generation. This keeps startup fast and memory low, and makes applications work in GraalVM native images without reflection configuration.

Micronaut Serialization applies the same idea to JSON. (De)serializers are generated at compile time, and a small streaming backend does the tokenizing and writing; today that backend is usually jackson-core.

Fory JSON's Utf8JsonReader / Utf8JsonWriter are a very good fit for that backend role. We built a Micronaut Serialization backend on top of them that keeps Micronaut's generated (de)serializers and uses none of Fory's codecs. On Micronaut's user-bean benchmark it is clearly faster than the same generated code on jackson-core; numbers are below. It passes Micronaut Serialization's full TCK on the JVM and in a GraalVM native image.

The obstacle is that there is no supported way to obtain a reader or writer outside ForyJson's object mapping. Creating one needs a JsonConfig (no public constructor; ForyJson.config() is package-private) and a JsonTypeResolver. Our prototype had to put a helper class into the org.apache.fory.json package (a split package that breaks on the module path) and create its own reader instances outside ForyJson's fixed pool. As the comment in the ForyJson constructor explains, that pool is intentionally the only owner of execution state, so that field-name caches stay bounded.

This PR adds the smallest supported entry point for that use case and changes nothing else. It is a first step for #4164. A standalone reader/writer artifact can be discussed separately there.

What does this PR do?

  • Adds ForyJson.readUtf8(byte[] bytes, int offset, int length, Utf8ReadOperation<R, E> operation):
    • lends the pooled Utf8JsonReader for the byte range to the operation;
    • rejects trailing non-whitespace content afterwards;
    • returns the operation's result.
  • Adds ForyJson.writeUtf8(Utf8WriteOperation<E> operation), which lends the pooled Utf8JsonWriter and returns the written bytes as a detached array.
  • Adds two small functional interfaces, Utf8ReadOperation<R, E extends Throwable> and Utf8WriteOperation<E extends Throwable>. Checked exceptions thrown by the caller's (de)serializers, such as IOException, propagate unchanged without wrapping.
  • Both methods reuse the existing root-operation structure: acquire() / release() of the fixed pool, lockJIT() / unlockJIT(), and the same buffer handoff and cleanup as fromJson(byte[], int, int, …) and toJsonBytes(…). Field-name caches therefore stay bounded, and the readers and writers never escape the pool.
  • Tests in ForyJsonTokenOperationTest:
    • round trip through the lent reader and writer;
    • byte ranges;
    • trailing-content rejection;
    • checked and runtime exception propagation;
    • state release after failures, using a single-slot pool that would otherwise block;
    • pool reuse.
  • Docs: a "Token-level reading and writing" section in docs/json/integration.md. It includes a note that external readers should call enterDepth/exitDepth and reserveGraphMemory, as custom codecs do, for the depth and graph-memory limits to apply.

No existing code path is modified, and no existing public API changes.

Validation from the consumer side: we rebased the Micronaut Serialization backend onto exactly this API, using a local 1.8.0-SNAPSHOT build of this branch. It no longer needs the split-package helper or its own reader/writer pool. Micronaut's Fory module tests (1226) and the serialization TCK suite pass unchanged on top of readUtf8 / writeUtf8.

Related issues

AI Contribution Checklist

  • Substantial AI assistance was used in this PR: yes
  • If yes, I included the standardized AI Usage Disclosure block below.
  • If yes, I can explain and defend all important changes without AI help.
  • If yes, I reviewed AI-assisted code changes line by line before submission.
  • If yes, I completed line-by-line self-review first and fixed issues before requesting AI review.
  • If yes, I ran two fresh AI review agents on the current PR diff or current HEAD after the latest code changes: one Fory-guided reviewer using AGENTS.md and .agents/ci-and-pr.md, and one independent general reviewer in a separate clean-context review session that was not pointed to .agents/ci-and-pr.md or any copied Fory-specific review checklist. If the independent reviewer's tooling auto-loaded AGENTS.md, it followed the independent-review carve-out there.
  • If yes, I addressed all AI review comments and repeated the review loop until both ai reviewers reported no further actionable comments.
  • If yes, I attached screenshot evidence or equivalent persisted links of the final clean AI review results from both fresh reviewers on the current PR diff or current HEAD after the latest code changes in this PR body.
  • If yes, I ran adequate human verification and recorded evidence (checks run locally or in CI, pass/fail summary, and confirmation I reviewed results).
  • If yes, I added/updated tests and specs where required.
  • If yes, I validated protocol/performance impacts with evidence when applicable.
  • If yes, I verified licensing and provenance compliance.
AI Usage Disclosure
- substantial_ai_assistance: yes
- scope: code drafting, tests, docs, design discussion
- affected_files_or_subsystems: java/fory-json (ForyJson, Utf8ReadOperation, Utf8WriteOperation, ForyJsonTokenOperationTest), docs/json/integration.md
- ai_review: line-by-line self-review completed. Two fresh AI reviewers on current HEAD 1e77054e4: (1) Fory-guided reviewer (AGENTS.md + .agents/ci-and-pr.md); an earlier pass raised one docs finding (external readers must track depth/graph memory), fixed in 00d467e71; final result: "No further actionable findings". (2) Independent general reviewer in a separate clean session, not pointed at .agents/ci-and-pr.md; final result: "No further actionable findings in git diff upstream/main...HEAD at 1e77054e4".
- ai_review_artifacts: final clean review session ids 01a15908-107b-7572-b0cb-c01bcf91cef0 (Fory-guided) and 01a15909-ae88-7ac2-9d5b-ca3910be49d3 (independent), both on HEAD 1e77054e4; result summaries quoted above
- human_verification: `mvn -pl fory-json clean install` (JDK 25): 1289 tests pass, checkstyle, spotless, javadoc, JDK25 multi-release jar verifier and GraalVM feature jar verifier pass; license header check fixed in 1e77054e4; Micronaut Serialization backend rebased on this API: 1226 module tests + serialization TCK suite pass; results reviewed
- performance_verification: no existing code path is modified, so existing ForyJson performance is unchanged; consumer-side benchmark in the Benchmark section
- provenance_license_confirmation: Apache-2.0-compatible provenance confirmed; no third-party code introduced

Does this PR introduce any user-facing change?

  • Does this PR introduce any public API change? (additive only: ForyJson.readUtf8, ForyJson.writeUtf8, Utf8ReadOperation, Utf8WriteOperation)
  • Does this PR introduce any binary protocol compatibility change?

Benchmark

This PR does not modify existing code paths, so existing ForyJson performance is unaffected.

For context, these are consumer-side results from Micronaut Serialization's UserBeanSerdeBenchmark-style model (users with nested friends, tags and scalars). They compare the same compile-time-generated Micronaut (de)serializers running on jackson-core and on Fory's Utf8JsonReader / Utf8JsonWriter (JDK 25, 2 forks, 3×2s warmup, 5×2s measurement):

Stack serialize (ops/µs, higher is better) deserialize (µs/op, lower is better)
Micronaut generated serdes on jackson-core 0.525 3.047
Micronaut generated serdes on Fory reader/writer 1.257 (2.4×) 2.344 (−23%)
ForyJson's own generated codecs (reference) 1.890 0.933

Add ForyJson.readUtf8(bytes, offset, length, operation) and
ForyJson.writeUtf8(operation), which lend a pooled Utf8JsonReader or
Utf8JsonWriter to caller code. This lets frameworks that bring their own
(de)serializers, for example ones generated at compile time, use Fory JSON's
reader and writer through a supported API instead of reaching package-private
JsonConfig and JsonTypeResolver.

The operations reuse the existing fixed pool, so reader field-name caches stay
bounded, follow the same lockJIT/cleanup discipline as the other root APIs,
reject trailing content after reads, and propagate the operation's checked
exceptions unchanged. Existing ForyJson code paths are not modified.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant