Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@
* A container for additional, undeclared properties.
* This is a holder for any undeclared properties as specified with
* the 'additionalProperties' keyword in the OAS document.
*
* Transient (on models without children): the bag is read and written by this model's
* TypeAdapterFactory, so gson's reflection does not need to see it. Gson collects the
* declared fields of every class in the hierarchy and rejects two bound to one JSON
* name, which is what an allOf child - declaring the field itself and inheriting it -
* used to hit. A parent with children has no factory of its own, so it keeps the field
* bound for reflection; the child excludes only its own copy, leaving exactly one.

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: With a three-level allOf inheritance chain, the leaf still encounters duplicate additionalProperties fields: every ancestor with children remains non-transient, while only the leaf is hidden. Hide all but one field per inheritance hierarchy and add a multi-level regression test.

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/additional_properties.mustache, line 12:

<comment>With a three-level `allOf` inheritance chain, the leaf still encounters duplicate `additionalProperties` fields: every ancestor with children remains non-transient, while only the leaf is hidden. Hide all but one field per inheritance hierarchy and add a multi-level regression test.</comment>

<file context>
@@ -5,10 +5,11 @@
+   * declared fields of every class in the hierarchy and rejects two bound to one JSON
+   * name, which is what an allOf child - declaring the field itself and inheriting it -
+   * used to hit. A parent with children has no factory of its own, so it keeps the field
+   * bound for reflection; the child excludes only its own copy, leaving exactly one.
    */
   private {{^hasChildren}}transient {{/hasChildren}}Map<String, Object> additionalProperties;
</file context>

*/
private Map<String, Object> additionalProperties;
private {{^hasChildren}}transient {{/hasChildren}}Map<String, Object> additionalProperties;

/**
* Set the additional (undeclared) property with the specified name and value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2024,6 +2024,41 @@ public void testJdkHttpClientWithAndWithoutParentExtension() {
.content().contains("public class AnotherChild {");
}

@Test
public void testAdditionalPropertiesFieldIsTransientForGson() {
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("src/test/resources/3_0/allOf_extension_parent.yaml")
.setOutputDir(output.toString().replace("\\", "/"));

DefaultGenerator generator = new DefaultGenerator();
generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true");
generator.opts(configurator.toClientOptInput()).generate();

// gson's reflective adapter refuses a class with two JSON fields of one name; without
// `transient` an allOf child declares additionalProperties itself and inherits it too,
// making it undeserializable ("declares multiple JSON fields named
// 'additionalProperties'"). The child's bag is read and written by its own
// TypeAdapterFactory, so hiding the field from reflection changes nothing else.
assertThat(output.resolve("src/main/java/xyz/abcdef/model/Child.java"))
.content()
.contains("public class Child extends Person {")
.contains("private transient Map<String, Object> additionalProperties;");
// a parent with children gets no TypeAdapterFactory of its own ({{^hasChildren}} in
// pojo.mustache), so its field stays visible to reflection - the child's transient
// declaration shadows it, and no duplicate JSON field arises
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/Person.java"))
.content().doesNotContain("private transient Map<String, Object> additionalProperties;");
}

@Test
public void allOfWithSeveralRefsAndRefAsParentInAllOfNormalizationIsTrue() {
final Path output = newTempFolder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,15 @@ public void setArrayOfStrings(@javax.annotation.Nonnull List<String> arrayOfStri
* A container for additional, undeclared properties.
* This is a holder for any undeclared properties as specified with
* the 'additionalProperties' keyword in the OAS document.
*
* Transient (on models without children): the bag is read and written by this model's
* TypeAdapterFactory, so gson's reflection does not need to see it. Gson collects the
* declared fields of every class in the hierarchy and rejects two bound to one JSON
* name, which is what an allOf child - declaring the field itself and inheriting it -
* used to hit. A parent with children has no factory of its own, so it keeps the field
* bound for reflection; the child excludes only its own copy, leaving exactly one.
*/
private Map<String, Object> additionalProperties;
private transient Map<String, Object> additionalProperties;

/**
* Set the additional (undeclared) property with the specified name and value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ public void setColor(@javax.annotation.Nullable String color) {
* A container for additional, undeclared properties.
* This is a holder for any undeclared properties as specified with
* the 'additionalProperties' keyword in the OAS document.
*
* Transient (on models without children): the bag is read and written by this model's
* TypeAdapterFactory, so gson's reflection does not need to see it. Gson collects the
* declared fields of every class in the hierarchy and rejects two bound to one JSON
* name, which is what an allOf child - declaring the field itself and inheriting it -
* used to hit. A parent with children has no factory of its own, so it keeps the field
* bound for reflection; the child excludes only its own copy, leaving exactly one.
*/
private Map<String, Object> additionalProperties;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,15 @@ public void setRefArrayPrefixItems(@javax.annotation.Nullable List<Object> refAr
* A container for additional, undeclared properties.
* This is a holder for any undeclared properties as specified with
* the 'additionalProperties' keyword in the OAS document.
*
* Transient (on models without children): the bag is read and written by this model's
* TypeAdapterFactory, so gson's reflection does not need to see it. Gson collects the
* declared fields of every class in the hierarchy and rejects two bound to one JSON
* name, which is what an allOf child - declaring the field itself and inheriting it -
* used to hit. A parent with children has no factory of its own, so it keeps the field
* bound for reflection; the child excludes only its own copy, leaving exactly one.
*/
private Map<String, Object> additionalProperties;
private transient Map<String, Object> additionalProperties;

/**
* Set the additional (undeclared) property with the specified name and value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,15 @@ public void setArrayFooThree(@javax.annotation.Nullable List<Tag> arrayFooThree)
* A container for additional, undeclared properties.
* This is a holder for any undeclared properties as specified with
* the 'additionalProperties' keyword in the OAS document.
*
* Transient (on models without children): the bag is read and written by this model's
* TypeAdapterFactory, so gson's reflection does not need to see it. Gson collects the
* declared fields of every class in the hierarchy and rejects two bound to one JSON
* name, which is what an allOf child - declaring the field itself and inheriting it -
* used to hit. A parent with children has no factory of its own, so it keeps the field
* bound for reflection; the child excludes only its own copy, leaving exactly one.
*/
private Map<String, Object> additionalProperties;
private transient Map<String, Object> additionalProperties;

/**
* Set the additional (undeclared) property with the specified name and value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,15 @@ public void setDeclawed(@javax.annotation.Nullable Boolean 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.
*
* Transient (on models without children): the bag is read and written by this model's
* TypeAdapterFactory, so gson's reflection does not need to see it. Gson collects the
* declared fields of every class in the hierarchy and rejects two bound to one JSON
* name, which is what an allOf child - declaring the field itself and inheriting it -
* used to hit. A parent with children has no factory of its own, so it keeps the field
* bound for reflection; the child excludes only its own copy, leaving exactly one.
*/
private Map<String, Object> additionalProperties;
private transient Map<String, Object> additionalProperties;

/**
* Set the additional (undeclared) property with the specified name and value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,15 @@ public void setName(@javax.annotation.Nullable String name) {
* A container for additional, undeclared properties.
* This is a holder for any undeclared properties as specified with
* the 'additionalProperties' keyword in the OAS document.
*
* Transient (on models without children): the bag is read and written by this model's
* TypeAdapterFactory, so gson's reflection does not need to see it. Gson collects the
* declared fields of every class in the hierarchy and rejects two bound to one JSON
* name, which is what an allOf child - declaring the field itself and inheriting it -
* used to hit. A parent with children has no factory of its own, so it keeps the field
* bound for reflection; the child excludes only its own copy, leaving exactly one.
*/
private Map<String, Object> additionalProperties;
private transient Map<String, Object> additionalProperties;

/**
* Set the additional (undeclared) property with the specified name and value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,15 @@ public void setProp1(@javax.annotation.Nullable CircularReference2 prop1) {
* A container for additional, undeclared properties.
* This is a holder for any undeclared properties as specified with
* the 'additionalProperties' keyword in the OAS document.
*
* Transient (on models without children): the bag is read and written by this model's
* TypeAdapterFactory, so gson's reflection does not need to see it. Gson collects the
* declared fields of every class in the hierarchy and rejects two bound to one JSON
* name, which is what an allOf child - declaring the field itself and inheriting it -
* used to hit. A parent with children has no factory of its own, so it keeps the field
* bound for reflection; the child excludes only its own copy, leaving exactly one.
*/
private Map<String, Object> additionalProperties;
private transient Map<String, Object> additionalProperties;

/**
* Set the additional (undeclared) property with the specified name and value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,15 @@ public void setProp1(@javax.annotation.Nullable CircularReference3 prop1) {
* A container for additional, undeclared properties.
* This is a holder for any undeclared properties as specified with
* the 'additionalProperties' keyword in the OAS document.
*
* Transient (on models without children): the bag is read and written by this model's
* TypeAdapterFactory, so gson's reflection does not need to see it. Gson collects the
* declared fields of every class in the hierarchy and rejects two bound to one JSON
* name, which is what an allOf child - declaring the field itself and inheriting it -
* used to hit. A parent with children has no factory of its own, so it keeps the field
* bound for reflection; the child excludes only its own copy, leaving exactly one.
*/
private Map<String, Object> additionalProperties;
private transient Map<String, Object> additionalProperties;

/**
* Set the additional (undeclared) property with the specified name and value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,15 @@ public void setProp1(@javax.annotation.Nullable CircularReference1 prop1) {
* A container for additional, undeclared properties.
* This is a holder for any undeclared properties as specified with
* the 'additionalProperties' keyword in the OAS document.
*
* Transient (on models without children): the bag is read and written by this model's
* TypeAdapterFactory, so gson's reflection does not need to see it. Gson collects the
* declared fields of every class in the hierarchy and rejects two bound to one JSON
* name, which is what an allOf child - declaring the field itself and inheriting it -
* used to hit. A parent with children has no factory of its own, so it keeps the field
* bound for reflection; the child excludes only its own copy, leaving exactly one.
*/
private Map<String, Object> additionalProperties;
private transient Map<String, Object> additionalProperties;

/**
* Set the additional (undeclared) property with the specified name and value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,15 @@ public void setBreed(@javax.annotation.Nullable String 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.
*
* Transient (on models without children): the bag is read and written by this model's
* TypeAdapterFactory, so gson's reflection does not need to see it. Gson collects the
* declared fields of every class in the hierarchy and rejects two bound to one JSON
* name, which is what an allOf child - declaring the field itself and inheriting it -
* used to hit. A parent with children has no factory of its own, so it keeps the field
* bound for reflection; the child excludes only its own copy, leaving exactly one.
*/
private Map<String, Object> additionalProperties;
private transient Map<String, Object> additionalProperties;

/**
* Set the additional (undeclared) property with the specified name and value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,15 @@ public void setEvent(@javax.annotation.Nonnull FakeWebhooksSourcesDeletedPostReq
* A container for additional, undeclared properties.
* This is a holder for any undeclared properties as specified with
* the 'additionalProperties' keyword in the OAS document.
*
* Transient (on models without children): the bag is read and written by this model's
* TypeAdapterFactory, so gson's reflection does not need to see it. Gson collects the
* declared fields of every class in the hierarchy and rejects two bound to one JSON
* name, which is what an allOf child - declaring the field itself and inheriting it -
* used to hit. A parent with children has no factory of its own, so it keeps the field
* bound for reflection; the child excludes only its own copy, leaving exactly one.
*/
private Map<String, Object> additionalProperties;
private transient Map<String, Object> additionalProperties;

/**
* Set the additional (undeclared) property with the specified name and value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,15 @@ public void setEventId(@javax.annotation.Nonnull String eventId) {
* A container for additional, undeclared properties.
* This is a holder for any undeclared properties as specified with
* the 'additionalProperties' keyword in the OAS document.
*
* Transient (on models without children): the bag is read and written by this model's
* TypeAdapterFactory, so gson's reflection does not need to see it. Gson collects the
* declared fields of every class in the hierarchy and rejects two bound to one JSON
* name, which is what an allOf child - declaring the field itself and inheriting it -
* used to hit. A parent with children has no factory of its own, so it keeps the field
* bound for reflection; the child excludes only its own copy, leaving exactly one.
*/
private Map<String, Object> additionalProperties;
private transient Map<String, Object> additionalProperties;

/**
* Set the additional (undeclared) property with the specified name and value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,15 @@ public void setMessage(@javax.annotation.Nullable String message) {
* A container for additional, undeclared properties.
* This is a holder for any undeclared properties as specified with
* the 'additionalProperties' keyword in the OAS document.
*
* Transient (on models without children): the bag is read and written by this model's
* TypeAdapterFactory, so gson's reflection does not need to see it. Gson collects the
* declared fields of every class in the hierarchy and rejects two bound to one JSON
* name, which is what an allOf child - declaring the field itself and inheriting it -
* used to hit. A parent with children has no factory of its own, so it keeps the field
* bound for reflection; the child excludes only its own copy, leaving exactly one.
*/
private Map<String, Object> additionalProperties;
private transient Map<String, Object> additionalProperties;

/**
* Set the additional (undeclared) property with the specified name and value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,15 @@ public void setComplete(@javax.annotation.Nullable Boolean complete) {
* A container for additional, undeclared properties.
* This is a holder for any undeclared properties as specified with
* the 'additionalProperties' keyword in the OAS document.
*
* Transient (on models without children): the bag is read and written by this model's
* TypeAdapterFactory, so gson's reflection does not need to see it. Gson collects the
* declared fields of every class in the hierarchy and rejects two bound to one JSON
* name, which is what an allOf child - declaring the field itself and inheriting it -
* used to hit. A parent with children has no factory of its own, so it keeps the field
* bound for reflection; the child excludes only its own copy, leaving exactly one.
*/
private Map<String, Object> additionalProperties;
private transient Map<String, Object> additionalProperties;

/**
* Set the additional (undeclared) property with the specified name and value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,15 @@ public void setStatus(@javax.annotation.Nullable StatusEnum status) {
* A container for additional, undeclared properties.
* This is a holder for any undeclared properties as specified with
* the 'additionalProperties' keyword in the OAS document.
*
* Transient (on models without children): the bag is read and written by this model's
* TypeAdapterFactory, so gson's reflection does not need to see it. Gson collects the
* declared fields of every class in the hierarchy and rejects two bound to one JSON
* name, which is what an allOf child - declaring the field itself and inheriting it -
* used to hit. A parent with children has no factory of its own, so it keeps the field
* bound for reflection; the child excludes only its own copy, leaving exactly one.
*/
private Map<String, Object> additionalProperties;
private transient Map<String, Object> additionalProperties;

/**
* Set the additional (undeclared) property with the specified name and value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,15 @@ public void setSecondProperty(@javax.annotation.Nullable Object secondProperty)
* A container for additional, undeclared properties.
* This is a holder for any undeclared properties as specified with
* the 'additionalProperties' keyword in the OAS document.
*
* Transient (on models without children): the bag is read and written by this model's
* TypeAdapterFactory, so gson's reflection does not need to see it. Gson collects the
* declared fields of every class in the hierarchy and rejects two bound to one JSON
* name, which is what an allOf child - declaring the field itself and inheriting it -
* used to hit. A parent with children has no factory of its own, so it keeps the field
* bound for reflection; the child excludes only its own copy, leaving exactly one.
*/
private Map<String, Object> additionalProperties;
private transient Map<String, Object> additionalProperties;

/**
* Set the additional (undeclared) property with the specified name and value.
Expand Down
Loading
Loading