Repository navigation
feat(java): reject repeated JSON properties on read - #4166
Open
pavel-ptashyts wants to merge 1 commit into
Open
pavel-ptashyts wants to merge 1 commit into
pavel-ptashyts wants to merge 1 commit into
Conversation
Add ForyJsonBuilder.failOnDuplicateProperties(boolean) and the class annotation @JsonDuplicateProperties(fail = ...). When enabled for a type, a known property (field, setter, creator parameter or unwrapped name) that appears twice in one JSON object fails the read with a ForyJsonException naming the property. The default is unchanged: the last occurrence wins, and lenient types generate the same reader code as before. Strict generated readers carry one bit per member through the fast path, its group helpers and the slow method; the interpreter keeps a per-object bit set. The effective setting is part of the generated codec key. Closes apache#4165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pavel-ptashyts
requested review from
PragmaTwice,
chaokunyang and
theweipeng
as code owners
October 11, 2026 17:44
Contributor
Author
AI review artifact: Fory-guided reviewer (final round)
Session header: Final output: |
Contributor
Author
AI review artifact: independent general reviewer (final round)
Final output: Review complete. I read the full diff and the surrounding interpreter and codegen code, traced every reader path the feature touches, and found no correctness bugs. What I verified
Low-severity note, not actionable. A strict class with zero known members and no unwrapped routes generates an empty No further actionable comments. |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why?
When a JSON object repeats one of a bean's known properties,
fory-jsonkeeps the last value and there is no way to reject the document instead. Parsers disagree on duplicate names (first wins, last wins, or fail), so a service that validates a document from a third party and passes it on can read a different value than the next hop. Jackson only offers the parser-wideSTRICT_DUPLICATE_DETECTION, which applies to every object and map and costs every read. Details and the motivating case (rejecting repeated top-level fields of inbound OpenRTB requests) are in #4165.What does this PR do?
ForyJsonBuilder.failOnDuplicateProperties(boolean), defaultfalse(unchanged behaviour: the last occurrence wins), and a class annotation@JsonDuplicateProperties(fail = true | false)that overrides the builder for one class. Like@JsonInclude, the annotation is not inherited and an exact-target Mixin can supply it; it is registered in the runtime and annotation-processor Mixin lists. It applies where the class is read as a JSON object.@JsonUnwrapped) that appears twice in one object fails the read withForyJsonException: Duplicate JSON property <name> on <class>. An explicitnullcounts as an occurrence, also underonNullRead(SKIP). Unknown and ignored names and Any entries are not tracked. Nested objects follow their own class setting.ObjectCodec). Strict ordinary beans use their own per-representation loop (read*StrictFixedObject): one extra check inside the lenient bean loop measured about +8% on interpreted reads, so the lenient loop is left exactly as it was. Creator, Any and unwrapped loops check a bit set that is allocated only for strict types; their lenient timings stayed within noise.JsonReaderCodegen). The decision is made at generation time, so lenient types generate exactly the same source as before. Strict bean readers keeplongwordsseen0..seenK: the fast path (which never repeats a member) only sets bits, group helpers receive the words as parameters and return their own group-relative bits as along(-1when the object is finished), and slow methods take the words as parameters and check every resolved known index. Ordered creator prefixes have no look-ahead, so the creator slow method seeds its bits from the prefix length. Unwrapped and Any-creator readers keep the words as locals of their single loop. Names for runtime-index failures come from a cold generatedswitch.docs/json/object-mapping.md(builder option) anddocs/json/annotations.md(annotation).JsonDuplicatePropertiesTest(both codegen modes, Latin1/UTF-16/UTF-8 String/bytes/stream inputs): beans, explicit nulls, class override in both directions, nested objects, ordered and unordered creators, Any beans and Any creators, unwrapped with creators and prefixes, unwrapped with Any, a subtype with Any, a generic bean, a Mixin, a 70-property bean whose group straddles the 64-bit word boundary, a slow fallback from inside a group helper, long (non-direct) names, and two instances with different settings in one JVM.JsonGeneratedCodecTest.duplicateTrackingSourcechecks the generated Latin1, UTF-16 and UTF-8 reader sources: no tracking for a lenient class, tracking for a strict one.Related issues
AI Contribution Checklist
yesyes, I included a completed AI Contribution Checklist in this PR description and the requiredAI Usage Disclosure.yes, my PR description includes the requiredai_reviewsummary and screenshot evidence or equivalent persisted links of the final clean AI review results from both fresh reviewers described inAI_POLICY.md, the Fory-guided reviewer and the independent general reviewer, on the current PR diff or current HEAD after the latest code changes.Completed checklist from
AI_POLICY.mdsection 9:yesAI Usage Disclosureblock below.AGENTS.mdand.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.mdor any copied Fory-specific review checklist.Verification
JDK 25.0.2, Windows 11 x86_64, from
java/:mvn -pl fory-json -am install -DskipTests: successmvn -pl fory-json,fory-annotation-processor test: fory-json 1324 tests, fory-annotation-processor 102 tests, 0 failures, 0 errors, 0 skippedmvn -pl fory-json,fory-annotation-processor spotless:check checkstyle:check: cleanJsonDuplicatePropertiesTestfail as intended: no interpreter marks; no generated marks; no fast-path marks; no second-word carry of group bits; group bits not passed from a helper to the slow method; setting removed from the generated codec key.FORY_CODE_DIR, the 30 generated sources for a bean, an Any bean, a creator, an unwrapped model and a 70-property bean (all writer and reader roles) are byte-identical betweenapache/main(b602392) and this branch.Does this PR introduce any user-facing change?
ForyJsonBuilder.failOnDuplicateProperties(boolean)and@JsonDuplicateProperties; the default behaviour is unchanged.Benchmark
Default (lenient) path: generated readers are byte-identical to
apache/main(see above). Interpreted lenient reads (withCodegen(false), 24intfields, UTF-8 bytes, ad-hoc timing loop, median of 6 alternating JVMs per jar): declaration-ordered document 343 ns on main vs 340 ns on this branch; document with the first member last 384 vs 365 ns (that case is bimodal across JVMs on both jars). Interpreted 10-member creator and Any bean: within noise.Strict readers, generated code (same 24-field bean, strict vs lenient class in one JVM): declaration-ordered document within noise; document with the first member moved to the end about +5..10%.