Conversation
…Of child
With disallowAdditionalPropertiesIfNotPresent=false every model declares
private Map<String, Object> additionalProperties - so an allOf child
declares the field twice, once itself and once inherited, and gson refuses
the class before the model's own TypeAdapterFactory can run:
IllegalArgumentException: Class Child declares multiple JSON fields
named 'additionalProperties'
Every allOf child model is undeserializable (and unserializable). Mark the
field transient: the custom TypeAdapterFactory already reads and writes the
bag explicitly around the reflective delegate - on write it even removes
the additionalProperties key the delegate emitted - so hiding it from
gson's reflection changes nothing for flat models and unbreaks the children.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GcwZ1arjLZNpetHz2a3TJz
There was a problem hiding this comment.
1 issue found across 147 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/NumberOnly.java">
<violation number="1" location="samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/NumberOnly.java:90">
P2: The added JavaClientCodegenTest#testAdditionalPropertiesFieldIsTransientForGson does not validate the behavior the PR claims it verifies. It only asserts that the generated Child/Person source contains the literal text "private transient Map<String, Object> additionalProperties;". It never constructs a Gson instance, never runs fromJson/toJson, and never checks the bag round-trips, despite the PR description saying it "Verifies gson.fromJson round-trips with the bag populated; extra/absent properties behave as before." As written, the test would pass even if the allOf deserialization were still broken, so the regression is not actually guarded. Add a real round-trip assertion (e.g., generate the models, then use JSON.getGson().fromJson on a Child with a header field and verify getAdditionalProperties()/serialization output), or correct the PR/test description.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| * parent) from declaring two JSON fields of one name. | ||
| */ | ||
| private Map<String, Object> additionalProperties; | ||
| private transient Map<String, Object> additionalProperties; |
There was a problem hiding this comment.
P2: The added JavaClientCodegenTest#testAdditionalPropertiesFieldIsTransientForGson does not validate the behavior the PR claims it verifies. It only asserts that the generated Child/Person source contains the literal text "private transient Map<String, Object> additionalProperties;". It never constructs a Gson instance, never runs fromJson/toJson, and never checks the bag round-trips, despite the PR description saying it "Verifies gson.fromJson round-trips with the bag populated; extra/absent properties behave as before." As written, the test would pass even if the allOf deserialization were still broken, so the regression is not actually guarded. Add a real round-trip assertion (e.g., generate the models, then use JSON.getGson().fromJson on a Child with a header field and verify getAdditionalProperties()/serialization output), or correct the PR/test description.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/NumberOnly.java, line 90:
<comment>The added JavaClientCodegenTest#testAdditionalPropertiesFieldIsTransientForGson does not validate the behavior the PR claims it verifies. It only asserts that the generated Child/Person source contains the literal text "private transient Map<String, Object> additionalProperties;". It never constructs a Gson instance, never runs fromJson/toJson, and never checks the bag round-trips, despite the PR description saying it "Verifies gson.fromJson round-trips with the bag populated; extra/absent properties behave as before." As written, the test would pass even if the allOf deserialization were still broken, so the regression is not actually guarded. Add a real round-trip assertion (e.g., generate the models, then use JSON.getGson().fromJson on a Child with a header field and verify getAdditionalProperties()/serialization output), or correct the PR/test description.</comment>
<file context>
@@ -82,8 +82,12 @@ public void setJustNumber(@javax.annotation.Nullable BigDecimal justNumber) {
+ * parent) from declaring two JSON fields of one name.
*/
- private Map<String, Object> additionalProperties;
+ private transient Map<String, Object> additionalProperties;
/**
</file context>
There was a problem hiding this comment.
Fair. The codegen test asserts generated source, which is the convention in JavaClientCodegenTest and what makes it cheap to run in this suite. The behavioural claim - gson.fromJson of an allOf child round-trips with the bag populated, absent and extra properties unchanged - was verified end to end against a generated client with a real Gson instance, as described in the PR body; that verification does not have a natural home in this test class, but I am glad to add a runtime test under the java sample suites if you would like it pinned in CI.
…heir own
Review caught that models with children get no CustomTypeAdapterFactory
({{^hasChildren}} in pojo.mustache), so a transient field on such a parent
silently dropped its bag - reflection was its only carrier. The transient
modifier is now gated the same way the factory is: parents with children
keep the field visible (their reflective round-trip is unchanged), and an
allOf child's transient declaration shadows it, so the duplicate-field
conflict still cannot arise.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GcwZ1arjLZNpetHz2a3TJz
|
cubic's P1 was a real hole, now fixed: a model with children gets no On the other remarks:
|
There was a problem hiding this comment.
1 existing issue remains and no new issues found across 147 files (changes from recent commits).
Requires human review: Auto-approval blocked because this review re-detected 2 unresolved issues already reported by Cubic.
Re-trigger cubic
Gson does not use Java field-hiding semantics: it collects the declared fields of each class in the hierarchy and refuses two bound to one JSON name, so the child's transient declaration excludes its own copy rather than hiding the parent's. Same behaviour, correct description - the comment ships into every generated model. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GcwZ1arjLZNpetHz2a3TJz
There was a problem hiding this comment.
All reported issues were addressed across 146 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Review caught that a three-level allOf chain (Root <- Middle <- Leaf) still
broke: only models without children marked their copy transient, so Root
and Middle - both parents - each kept a reflectively bound
additionalProperties field, and gson refused Middle and Leaf alike
("declares multiple JSON fields named 'additionalProperties'").
Marking Middle's copy transient as well would not do: a parent has no
TypeAdapterFactory, so reflection would fill Root's field while Middle's
accessors read its own, dropping the bag. Instead the field is now declared
exactly once, on the topmost ancestor that has one; JavaClientCodegen tags
every descendant with x-inherits-additional-properties, and those models
inherit the field and its getters, override putAdditionalProperty only to
keep the covariant return type, and leave the bag to super.equals,
super.hashCode and super.toString.
Adds a multi-level regression test (fails without this change) and
regenerates the okhttp-gson samples.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
1 existing issue remains and no new issues found across 150 files (changes from recent commits).
Requires human review: Auto-approval blocked because this review re-detected 2 unresolved issues already reported by Cubic.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…ildren
Review caught that a descendant inherits the bag from a class that has children, so
the field is not transient and gson's reflective delegate binds it. An undeclared
property literally named `additionalProperties` therefore landed in the bag twice:
the delegate stored it under its own name, and the descendant's adapter then read the
same key from the raw JSON and flattened its entries into the bag. Writing back
emitted those entries as top-level properties, so
{"className":"Dog","breed":"b","additionalProperties":{"x":1},"extra":"e"} came out
with a phantom top-level "x":1.0.
The adapter now clears the delegate-bound bag right after fromJsonTree, before the
loop that collects the undeclared keys. That loop re-reads every one of them from the
raw JSON, the literal key included, so nothing is lost and it round-trips as a single
entry. The clear is emitted only for models tagged
x-inherits-additional-properties: a model that declares the bag itself keeps it
transient, so the delegate binds nothing into it and there is nothing to clear.
Extends the codegen tests with the emitted clear and adds a runtime round-trip to the
okhttp-gson sample's hand-written JSONTest.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed across 9 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…son delegate On an allOf child the inherited bag is visible to gson's reflection, so the delegate adapter tried to bind a literal `additionalProperties` key into the Map-typed field and threw on a primitive or array value. The adapter now hands the delegate a copy without that key (only when present) and the extras loop collects it from the raw JSON like any other undeclared property. The delegate no longer binds anything into the bag, so the clear() is dropped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… okhttp-gson bag Declaring the additionalProperties field once per hierarchy already removes the duplicate that gson rejects, so `transient` fixed nothing: it only changed flat models, which were never broken (and dropped the bag from Java serialization under serializableModel). Flat models are byte-identical to master again. The delegate copy that hid a literal `additionalProperties` JSON key from the reflective adapter is removed too. Every okhttp-gson model on master binds that key through the delegate, so it is a pre-existing gap for a separate fix, not part of this one. allOf descendants now behave exactly like flat models for it. The codegen tests assert only the invariant: one class per hierarchy declares the bag, descendants override putAdditionalProperty. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… okhttp-gson JSONTest Replaces the two literal-`additionalProperties`-key cases with a plain Dog round-trip, the case that threw on master, and records the new checksum. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 150 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/pojo.mustache">
<violation number="1">
P1: This read path now delegates `jsonObj` directly to the reflective adapter, which re-exposes a literal `additionalProperties` key to the inherited bag. Because the bag is declared once on the topmost ancestor and is no longer `transient`, the child's `thisAdapter.fromJsonTree` binds a literal `additionalProperties` key straight into the inherited `Map<String, Object>` field: non-object values (number, array, string, boolean) make Gson throw during deserialization, and object values are double-stored (the delegate fills the map, then the unchanged loop below calls `putAdditionalProperty("additionalProperties", …)` again), so the bag and the round-trip output gain spurious keys. This contradicts the PR goal, which calls for dropping the key before delegating so round-trips preserve the key once; the added JSONTest only exercises plain undeclared keys like `"extra"`, so it does not cover this. Restore the `delegateObj` deep copy + `remove("additionalProperties")` before `thisAdapter.fromJsonTree` for models with `x-inherits-additional-properties`.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| @@ -206,8 +206,8 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens | |||
| }{{#hasVars}} | |||
There was a problem hiding this comment.
P1: This read path now delegates jsonObj directly to the reflective adapter, which re-exposes a literal additionalProperties key to the inherited bag. Because the bag is declared once on the topmost ancestor and is no longer transient, the child's thisAdapter.fromJsonTree binds a literal additionalProperties key straight into the inherited Map<String, Object> field: non-object values (number, array, string, boolean) make Gson throw during deserialization, and object values are double-stored (the delegate fills the map, then the unchanged loop below calls putAdditionalProperty("additionalProperties", …) again), so the bag and the round-trip output gain spurious keys. This contradicts the PR goal, which calls for dropping the key before delegating so round-trips preserve the key once; the added JSONTest only exercises plain undeclared keys like "extra", so it does not cover this. Restore the delegateObj deep copy + remove("additionalProperties") before thisAdapter.fromJsonTree for models with x-inherits-additional-properties.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/pojo.mustache, line 567:
<comment>This read path now delegates `jsonObj` directly to the reflective adapter, which re-exposes a literal `additionalProperties` key to the inherited bag. Because the bag is declared once on the topmost ancestor and is no longer `transient`, the child's `thisAdapter.fromJsonTree` binds a literal `additionalProperties` key straight into the inherited `Map<String, Object>` field: non-object values (number, array, string, boolean) make Gson throw during deserialization, and object values are double-stored (the delegate fills the map, then the unchanged loop below calls `putAdditionalProperty("additionalProperties", …)` again), so the bag and the round-trip output gain spurious keys. This contradicts the PR goal, which calls for dropping the key before delegating so round-trips preserve the key once; the added JSONTest only exercises plain undeclared keys like `"extra"`, so it does not cover this. Restore the `delegateObj` deep copy + `remove("additionalProperties")` before `thisAdapter.fromJsonTree` for models with `x-inherits-additional-properties`.</comment>
<file context>
@@ -563,18 +563,8 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens
- {{/vendorExtensions.x-inherits-additional-properties}}
// store additional fields in the deserialized instance
- {{classname}} instance = thisAdapter.fromJsonTree({{#vendorExtensions.x-inherits-additional-properties}}delegateObj{{/vendorExtensions.x-inherits-additional-properties}}{{^vendorExtensions.x-inherits-additional-properties}}jsonObj{{/vendorExtensions.x-inherits-additional-properties}});
+ {{classname}} instance = thisAdapter.fromJsonTree(jsonObj);
for (Map.Entry<String, JsonElement> entry : jsonObj.entrySet()) {
if (!openapiFields.contains(entry.getKey())) {
</file context>
| }{{#hasVars}} | |
| {{#vendorExtensions.x-inherits-additional-properties}} | |
| // the inherited bag is visible to reflection, so hide a literal `additionalProperties` | |
| // key from the delegate (it would try to bind any JSON type into the Map); the loop | |
| // below still collects it from the raw JSON, like every other undeclared key | |
| JsonObject delegateObj = jsonObj; | |
| if (jsonObj.has("additionalProperties")) { | |
| delegateObj = jsonObj.deepCopy(); | |
| delegateObj.remove("additionalProperties"); | |
| } | |
| {{/vendorExtensions.x-inherits-additional-properties}} | |
| {{classname}} instance = thisAdapter.fromJsonTree({{#vendorExtensions.x-inherits-additional-properties}}delegateObj{{/vendorExtensions.x-inherits-additional-properties}}{{^vendorExtensions.x-inherits-additional-properties}}jsonObj{{/vendorExtensions.x-inherits-additional-properties}}); |
There was a problem hiding this comment.
This is the known gap the description lists: a literal additionalProperties JSON key holding a non-object value throws when gson's reflective delegate binds it to the bag field. It is not new to this PR. On master every model that declares the bag behaves the same way, because the field is visible to reflection there too; this PR keeps allOf children on that same behaviour instead of failing on the duplicate field. Fixing it for all models belongs in its own change.
With
disallowAdditionalPropertiesIfNotPresent=false,additional_properties.mustachedeclaresprivate Map<String, Object> additionalProperties;in every okhttp-gson model. An allOf child (Child extends Person) therefore declares the field twice, once itself and once inherited, and gson refuses the class as soon as anything asks for its adapter, before the model's ownCustomTypeAdapterFactorycan run:So every allOf child is undeserializable (and unserializable) under the flag. Reproduced on current master.
The fix
JavaClientCodegen.postProcessAllModels(okhttp-gson only) tags each model whoseparentModelchain carries the bag withx-inherits-additional-properties.putAdditionalPropertyonly to keep the covariant return type.equals,hashCodeandtoStringleave the bag tosuper.Root <- Middle <- Leaf) works too.Tests
JavaClientCodegenTest#testAdditionalPropertiesFieldIsDeclaredOncePerHierarchyForGson(3_0/allOf_extension_parent.yaml) and#testAdditionalPropertiesFieldIsDeclaredOnceAcrossMultiLevelAllOfForGson(new3_0/java/okhttp-gson-additional-properties-allof-chain.yaml) assert that one class per hierarchy declares the bag and that descendants overrideputAdditionalProperty.JSONTest#testAdditionalPropertiesRoundTripOnAllOfChildin theokhttp-gsonpetstore sample round-trips aDogwith an undeclared property; it throws on master.Known gaps
additionalPropertiesis bound by gson's reflective delegate on every okhttp-gson model (it is stored twice, and a primitive or array value throws). This is pre-existing on master for flat models, allOf children now behave the same way, and it deserves a separate fix.PR checklist
Cat,Dog,ParentPet).Merge note: some intermediate commit messages describe superseded iterations (
transient, aclear(), a delegate copy), so please squash with the PR title and this description rather than the commit list.Generated with Claude Code