Skip to content
Merged
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
2 changes: 1 addition & 1 deletion bin/utils/test_file_list.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
- filename: "samples/client/petstore/java/okhttp-gson/src/test/java/org/openapitools/client/ClientTest.java"
sha256: 325fdd5d7e2c97790c0fb44f712ab7b2ba022d7e1a5b0056f47b07f342682b6d
- filename: "samples/client/petstore/java/okhttp-gson/src/test/java/org/openapitools/client/JSONTest.java"
sha256: 08b089934586d0b1564e9e5940c3dc4f3ef0b79d191ba91d36ee2f07356eab38
sha256: ddd53d2f640aee7712299f1540779cdb508f0b9c1e96082191686e218eadb04c
- filename: "samples/client/petstore/java/okhttp-gson/src/test/java/org/openapitools/client/api/PetApiTest.java"
sha256: 8b1b8f2a2ad00ccb090873a94a5f73e328b98317d2ec715f53bd7a1accb2a023
- filename: "samples/client/petstore/java/okhttp-gson/src/test/java/org/openapitools/client/model/PetTest.java"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,25 @@ public CodegenModel fromModel(String name, Schema model) {
return codegenModel;
}

@Override
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
objs = super.postProcessAllModels(objs);
if (isLibrary(OKHTTP_GSON)) {
// gson refuses a class whose hierarchy declares two fields bound to one JSON name, so
// the okhttp-gson additionalProperties bag is declared once, on the topmost ancestor
// that has one; every descendant inherits it instead of declaring its own copy
for (CodegenModel cm : getAllModels(objs).values()) {
for (CodegenModel ancestor = cm.getParentModel(); ancestor != null; ancestor = ancestor.getParentModel()) {
if (ancestor.isAdditionalPropertiesTrue) {
cm.vendorExtensions.put("x-inherits-additional-properties", true);
break;
}
}
}
}
return objs;
}

@Override
public ModelsMap postProcessModelsEnum(ModelsMap objs) {
objs = super.postProcessModelsEnum(objs);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{{#isAdditionalPropertiesTrue}}
{{^vendorExtensions.x-inherits-additional-properties}}
/**
* A container for additional, undeclared properties.
* This is a holder for any undeclared properties as specified with
Expand Down Expand Up @@ -43,4 +44,21 @@
}
return this.additionalProperties.get(key);
}
{{/vendorExtensions.x-inherits-additional-properties}}
{{#vendorExtensions.x-inherits-additional-properties}}
/**
* Set the additional (undeclared) property with the specified name and value.
* If the property does not already exist, create it otherwise replace it.
* The bag is inherited from the superclass.
*
* @param key name of the property
* @param value value of the property
* @return the {{classname}} instance itself
*/
@Override
public {{classname}} putAdditionalProperty(String key, Object value) {
super.putAdditionalProperty(key, value);
return this;
}
{{/vendorExtensions.x-inherits-additional-properties}}
{{/isAdditionalPropertiesTrue}}
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens
}{{#hasVars}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
}{{#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}});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

{{classname}} {{classVarName}} = ({{classname}}) o;
return {{#vars}}{{#isByteArray}}Arrays{{/isByteArray}}{{^isByteArray}}Objects{{/isByteArray}}.equals(this.{{name}}, {{classVarName}}.{{name}}){{^-last}} &&
{{/-last}}{{/vars}}{{#isAdditionalPropertiesTrue}}&&
Objects.equals(this.additionalProperties, {{classVarName}}.additionalProperties){{/isAdditionalPropertiesTrue}}{{#parent}} &&
{{/-last}}{{/vars}}{{#isAdditionalPropertiesTrue}}{{^vendorExtensions.x-inherits-additional-properties}}&&
Objects.equals(this.additionalProperties, {{classVarName}}.additionalProperties){{/vendorExtensions.x-inherits-additional-properties}}{{/isAdditionalPropertiesTrue}}{{#parent}} &&
super.equals(o){{/parent}};{{/hasVars}}{{^hasVars}}
return {{#parent}}super.equals(o){{/parent}}{{^parent}}true{{/parent}};{{/hasVars}}
{{/useReflectionEqualsHashCode}}
Expand All @@ -223,7 +223,7 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens
return HashCodeBuilder.reflectionHashCode(this);
{{/useReflectionEqualsHashCode}}
{{^useReflectionEqualsHashCode}}
return Objects.hash({{#vars}}{{^isByteArray}}{{name}}{{/isByteArray}}{{#isByteArray}}Arrays.hashCode({{name}}){{/isByteArray}}{{^-last}}, {{/-last}}{{/vars}}{{#parent}}{{#hasVars}}, {{/hasVars}}super.hashCode(){{/parent}}{{#isAdditionalPropertiesTrue}}{{#hasVars}}, {{/hasVars}}{{^hasVars}}{{#parent}}, {{/parent}}{{/hasVars}}additionalProperties{{/isAdditionalPropertiesTrue}});
return Objects.hash({{#vars}}{{^isByteArray}}{{name}}{{/isByteArray}}{{#isByteArray}}Arrays.hashCode({{name}}){{/isByteArray}}{{^-last}}, {{/-last}}{{/vars}}{{#parent}}{{#hasVars}}, {{/hasVars}}super.hashCode(){{/parent}}{{#isAdditionalPropertiesTrue}}{{^vendorExtensions.x-inherits-additional-properties}}{{#hasVars}}, {{/hasVars}}{{^hasVars}}{{#parent}}, {{/parent}}{{/hasVars}}additionalProperties{{/vendorExtensions.x-inherits-additional-properties}}{{/isAdditionalPropertiesTrue}});
{{/useReflectionEqualsHashCode}}
}{{#vendorExtensions.x-jackson-optional-nullable-helpers}}

Expand All @@ -245,7 +245,9 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens
sb.append(" {{name}}: ").append({{#isPassword}}"*"{{/isPassword}}{{^isPassword}}toIndentedString({{name}}){{/isPassword}}).append("\n");
{{/vars}}
{{#isAdditionalPropertiesTrue}}
{{^vendorExtensions.x-inherits-additional-properties}}
sb.append(" additionalProperties: ").append(toIndentedString(additionalProperties)).append("\n");
{{/vendorExtensions.x-inherits-additional-properties}}
{{/isAdditionalPropertiesTrue}}
sb.append("}");
return sb.toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2024,6 +2024,61 @@ public void testJdkHttpClientWithAndWithoutParentExtension() {
.content().contains("public class AnotherChild {");
}

@Test
public void testAdditionalPropertiesFieldIsDeclaredOncePerHierarchyForGson() {
final Path output = generateOkHttpGsonWithAdditionalProperties("src/test/resources/3_0/allOf_extension_parent.yaml");

// exactly one class per hierarchy declares the bag; descendants inherit it
assertThat(output.resolve("src/main/java/xyz/abcdef/model/Person.java"))
.content().contains("private Map<String, Object> additionalProperties;");
assertThat(output.resolve("src/main/java/xyz/abcdef/model/Child.java"))
.content()
.contains("public class Child extends Person {")
.contains("public Child putAdditionalProperty(String key, Object value) {")
.doesNotContain("Map<String, Object> additionalProperties;");
// a model without an allOf parent declares its own bag, as before
assertThat(output.resolve("src/main/java/xyz/abcdef/model/PersonA.java"))
.content().contains("private Map<String, Object> additionalProperties;");
}

@Test
public void testAdditionalPropertiesFieldIsDeclaredOnceAcrossMultiLevelAllOfForGson() {
final Path output = generateOkHttpGsonWithAdditionalProperties("src/test/resources/3_0/java/okhttp-gson-additional-properties-allof-chain.yaml");

// exactly one class per hierarchy declares the bag, also across Root <- Middle <- Leaf
assertThat(output.resolve("src/main/java/xyz/abcdef/model/Root.java"))
.content().contains("private Map<String, Object> additionalProperties;");
assertThat(output.resolve("src/main/java/xyz/abcdef/model/Middle.java"))
.content()
.contains("public class Middle extends Root {")
.contains("public Middle putAdditionalProperty(String key, Object value) {")
.doesNotContain("Map<String, Object> additionalProperties;");
assertThat(output.resolve("src/main/java/xyz/abcdef/model/Leaf.java"))
.content()
.contains("public class Leaf extends Middle {")
.contains("public Leaf putAdditionalProperty(String key, Object value) {")
.doesNotContain("Map<String, Object> additionalProperties;");
}

private Path generateOkHttpGsonWithAdditionalProperties(String inputSpec) {
final Path output = newTempFolder();
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName(JAVA_GENERATOR)
// use default `okhttp-gson`
.addAdditionalProperty(CodegenConstants.API_PACKAGE, "xyz.abcdef.api")
.addAdditionalProperty(CodegenConstants.MODEL_PACKAGE, "xyz.abcdef.model")
.addAdditionalProperty(CodegenConstants.INVOKER_PACKAGE, "xyz.abcdef.invoker")
.addAdditionalProperty("disallowAdditionalPropertiesIfNotPresent", "false")
.setInputSpec(inputSpec)
.setOutputDir(output.toString().replace("\\", "/"));

DefaultGenerator generator = new DefaultGenerator();
generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true");
List<File> files = generator.opts(configurator.toClientOptInput()).generate();
validateJavaSourceFiles(files);
return output;
}

@Test
public void allOfWithSeveralRefsAndRefAsParentInAllOfNormalizationIsTrue() {
final Path output = newTempFolder();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
openapi: 3.0.3
info: {title: t, version: '1'}
paths:
/c:
get:
responses:
'200':
description: ok
content:
application/json:
schema: {$ref: '#/components/schemas/Leaf'}
components:
schemas:
Root:
type: object
required: [kind]
discriminator: {propertyName: kind}
properties:
kind: {type: string}
a: {type: string}
Middle:
allOf:
- $ref: '#/components/schemas/Root'
- type: object
properties:
b: {type: string}
discriminator: {propertyName: kind}
Leaf:
allOf:
- $ref: '#/components/schemas/Middle'
- type: object
properties:
c: {type: string}
Original file line number Diff line number Diff line change
Expand Up @@ -79,51 +79,21 @@ public void setDeclawed(@javax.annotation.Nullable Boolean declawed) {
this.declawed = declawed;
}

/**
* A container for additional, undeclared properties.
* This is a holder for any undeclared properties as specified with
* the 'additionalProperties' keyword in the OAS document.
*/
private Map<String, Object> additionalProperties;

/**
* Set the additional (undeclared) property with the specified name and value.
* If the property does not already exist, create it otherwise replace it.
* The bag is inherited from the superclass.
*
* @param key name of the property
* @param value value of the property
* @return the Cat instance itself
*/
@Override
public Cat putAdditionalProperty(String key, Object value) {
if (this.additionalProperties == null) {
this.additionalProperties = new HashMap<String, Object>();
}
this.additionalProperties.put(key, value);
super.putAdditionalProperty(key, value);
return this;
}

/**
* Return the additional (undeclared) property.
*
* @return a map of objects
*/
public Map<String, Object> getAdditionalProperties() {
return additionalProperties;
}

/**
* Return the additional (undeclared) property with the specified name.
*
* @param key name of the property
* @return an object
*/
public Object getAdditionalProperty(String key) {
if (this.additionalProperties == null) {
return null;
}
return this.additionalProperties.get(key);
}


@Override
public boolean equals(Object o) {
Expand All @@ -134,14 +104,13 @@ public boolean equals(Object o) {
return false;
}
Cat cat = (Cat) o;
return Objects.equals(this.declawed, cat.declawed)&&
Objects.equals(this.additionalProperties, cat.additionalProperties) &&
return Objects.equals(this.declawed, cat.declawed) &&
super.equals(o);
}

@Override
public int hashCode() {
return Objects.hash(declawed, super.hashCode(), additionalProperties);
return Objects.hash(declawed, super.hashCode());
}

@Override
Expand All @@ -150,7 +119,6 @@ public String toString() {
sb.append("class Cat {\n");
sb.append(" ").append(toIndentedString(super.toString())).append("\n");
sb.append(" declawed: ").append(toIndentedString(declawed)).append("\n");
sb.append(" additionalProperties: ").append(toIndentedString(additionalProperties)).append("\n");
sb.append("}");
return sb.toString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,51 +79,21 @@ public void setBreed(@javax.annotation.Nullable String breed) {
this.breed = breed;
}

/**
* A container for additional, undeclared properties.
* This is a holder for any undeclared properties as specified with
* the 'additionalProperties' keyword in the OAS document.
*/
private Map<String, Object> additionalProperties;

/**
* Set the additional (undeclared) property with the specified name and value.
* If the property does not already exist, create it otherwise replace it.
* The bag is inherited from the superclass.
*
* @param key name of the property
* @param value value of the property
* @return the Dog instance itself
*/
@Override
public Dog putAdditionalProperty(String key, Object value) {
if (this.additionalProperties == null) {
this.additionalProperties = new HashMap<String, Object>();
}
this.additionalProperties.put(key, value);
super.putAdditionalProperty(key, value);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
return this;
}

/**
* Return the additional (undeclared) property.
*
* @return a map of objects
*/
public Map<String, Object> getAdditionalProperties() {
return additionalProperties;
}

/**
* Return the additional (undeclared) property with the specified name.
*
* @param key name of the property
* @return an object
*/
public Object getAdditionalProperty(String key) {
if (this.additionalProperties == null) {
return null;
}
return this.additionalProperties.get(key);
}


@Override
public boolean equals(Object o) {
Expand All @@ -134,14 +104,13 @@ public boolean equals(Object o) {
return false;
}
Dog dog = (Dog) o;
return Objects.equals(this.breed, dog.breed)&&
Objects.equals(this.additionalProperties, dog.additionalProperties) &&
return Objects.equals(this.breed, dog.breed) &&
super.equals(o);
}

@Override
public int hashCode() {
return Objects.hash(breed, super.hashCode(), additionalProperties);
return Objects.hash(breed, super.hashCode());
}

@Override
Expand All @@ -150,7 +119,6 @@ public String toString() {
sb.append("class Dog {\n");
sb.append(" ").append(toIndentedString(super.toString())).append("\n");
sb.append(" breed: ").append(toIndentedString(breed)).append("\n");
sb.append(" additionalProperties: ").append(toIndentedString(additionalProperties)).append("\n");
sb.append("}");
return sb.toString();
}
Expand Down
Loading
Loading