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 @@ -489,4 +489,5 @@ public static enum ENUM_PROPERTY_NAMING_TYPE {camelCase, PascalCase, snake_case,
public static final String X_DISCRIMINATOR_VALUE = "x-discriminator-value";
public static final String X_ONE_OF_NAME = "x-one-of-name";
public static final String X_NULLABLE = "x-nullable";
public static final String X_SETTER_EXTRA_ANNOTATION = "x-setter-extra-annotation";
}
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ public class CodegenProperty implements Cloneable, IJsonSchemaValidationProperti
public boolean isDiscriminator;
public boolean isNew; // true when this property overrides an inherited property
public Boolean isOverridden; // true if the property is a parent property (not defined in child/current schema)
public boolean isSetSetterExtensionDeclared; // true if a setter annotation is declared for `array` and `uniqueItems`
@Getter @Setter
public List<String> _enum;
@Getter @Setter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4055,6 +4055,11 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo
property.required = required;
ModelUtils.syncValidationProperties(p, property);
property.setFormat(p.getFormat());
property.isSetSetterExtensionDeclared =
p.getUniqueItems() != null
&& p.getUniqueItems()
&& p.getExtensions() != null
&& p.getExtensions().containsKey(X_SETTER_EXTRA_ANNOTATION);
Comment on lines +4058 to +4062
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 13, 2026

Choose a reason for hiding this comment

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

P2: isSetSetterExtensionDeclared only checks extensions on the unaliased schema, so wrapper-level x-setter-extra-annotation declarations (single-item allOf or $ref wrapper) are missed even though those extensions are later merged. This leaves the flag false and allows default setter annotations to overwrite user-defined ones.

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/java/org/openapitools/codegen/DefaultCodegen.java, line 4058:

<comment>`isSetSetterExtensionDeclared` only checks extensions on the unaliased schema, so wrapper-level `x-setter-extra-annotation` declarations (single-item allOf or $ref wrapper) are missed even though those extensions are later merged. This leaves the flag false and allows default setter annotations to overwrite user-defined ones.</comment>

<file context>
@@ -4055,6 +4055,11 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo
         property.required = required;
         ModelUtils.syncValidationProperties(p, property);
         property.setFormat(p.getFormat());
+        property.isSetSetterExtensionDeclared =
+                p.getUniqueItems() != null
+                        && p.getUniqueItems()
</file context>
Suggested change
property.isSetSetterExtensionDeclared =
p.getUniqueItems() != null
&& p.getUniqueItems()
&& p.getExtensions() != null
&& p.getExtensions().containsKey(X_SETTER_EXTRA_ANNOTATION);
property.isSetSetterExtensionDeclared =
Boolean.TRUE.equals(p.getUniqueItems())
&& ((p.getExtensions() != null && p.getExtensions().containsKey(X_SETTER_EXTRA_ANNOTATION))
|| (original != null && original.getExtensions() != null
&& original.getExtensions().containsKey(X_SETTER_EXTRA_ANNOTATION)));
Fix with Cubic

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Actually, didn't get the point :/

X_SETTER_EXTRA_ANNOTATION could be inherited somehow?

        nicknames:
          type: array
          uniqueItems: true
          x-setter-extra-annotation: '@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = org.openapitools.tools.TrimmingStringSetDeserializer.class)'
          items:
            type: string

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.

@drewlakee I suggested checking both the unaliased schema and the "original" wrapper because of how openapi-generator handles references and inheritance.

In your example, if nicknames is defined directly as an array in a model, it works fine. However, consider a case where the extension is declared on a property that references another schema (the "wrapper" case):

components:
  schemas:
    UniqueList:
      type: array
      uniqueItems: true
      items:
        type: string

    MyModel:
      properties:
        nicknames:
          $ref: '#/components/schemas/UniqueList'
          x-setter-extra-annotation: '@MyCustomAnnotation'

Inside fromProperty, the generator first identifies the property. Then, at line 4080, it calls p = unaliasSchema(p). This replaces the property definition with the resolved UniqueList schema.

The logic you added at line 4058 checks p.getExtensions(). Because p has been resolved to UniqueList, it only sees extensions defined inside UniqueList. It misses the extension defined on the property in MyModel (which is stored in the original variable).

Even though the extensions are merged into the property.vendorExtensions map later in the method, property.isSetSetterExtensionDeclared is calculated early. If that boolean remains false, AbstractJavaCodegen will proceed to add the default @JsonDeserialize(as = LinkedHashSet.class) annotation, potentially conflicting with the custom one you intended to use.

Checking original ensures that if a developer places the extension on the property usage rather than the global schema definition, it is still respected.

Cubic's product documentation | AI review settings

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Well, now I got it, but don't think it could be an appropriate use-case.

It would be better to leave it for the maintainers to think about.


property.name = toVarName(name);
property.baseName = name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public enum VendorExtension {
X_SPRING_API_VERSION("x-spring-api-version", ExtensionLevel.OPERATION, "Value for 'version' attribute in @RequestMapping (for Spring 7 and above).", null),
X_SPRING_PROVIDE_ARGS("x-spring-provide-args", ExtensionLevel.OPERATION, "Allows adding additional hidden parameters in the API specification to allow access to content such as header values or properties", "empty array"),
X_DISCRIMINATOR_VALUE("x-discriminator-value", ExtensionLevel.MODEL, "Used with model inheritance to specify value for discriminator that identifies current model", ""),
X_SETTER_EXTRA_ANNOTATION("x-setter-extra-annotation", ExtensionLevel.FIELD, "Custom annotation that can be specified over java setter for specific field", "When field is array & uniqueItems, then this extension is used to add `@JsonDeserialize(as = LinkedHashSet.class)` over setter, otherwise no value"),
X_SETTER_EXTRA_ANNOTATION("x-setter-extra-annotation", ExtensionLevel.FIELD, "Custom annotation that can be specified over java setter for specific field", "When field is array & uniqueItems, then this extension is used to add `@JsonDeserialize(as = LinkedHashSet.class)` over setter. Default @JsonDeserialize can be overridden by a custom one. In other cases has no value"),
X_WEBCLIENT_BLOCKING("x-webclient-blocking", ExtensionLevel.OPERATION, "Specifies if method for specific operation should be blocking or non-blocking(ex: return `Mono<T>/Flux<T>` or `return T/List<T>/Set<T>` & execute `.block()` inside generated method)", "false"),
X_WEBCLIENT_RETURN_EXCEPT_LIST_OF_STRING("x-webclient-return-except-list-of-string", ExtensionLevel.OPERATION, "Specifies if method for specific operation should return the type except List<String> and Set<String>(ex: return type expect the `Mono<List<String>>/Flux<List<String>>` and `Mono<Set<String>>/Flux<Set<String>>`)", "false"),
X_TAGS("x-tags", ExtensionLevel.OPERATION, "Specify multiple swagger tags for operation", null),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
import java.util.stream.StreamSupport;

import static org.openapitools.codegen.CodegenConstants.X_IMPLEMENTS;
import static org.openapitools.codegen.CodegenConstants.X_SETTER_EXTRA_ANNOTATION;
import static org.openapitools.codegen.utils.CamelizeOption.*;
import static org.openapitools.codegen.utils.ModelUtils.getSchemaItems;
import static org.openapitools.codegen.utils.OnceLogger.once;
Expand Down Expand Up @@ -1953,9 +1954,9 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
} else if ("set".equals(property.containerType)) {
model.imports.add("LinkedHashSet");
model.imports.add("Arrays");
if ((!openApiNullable || !property.isNullable) && jackson) { // cannot be wrapped to nullable
if ((!openApiNullable || !property.isNullable) && jackson && !property.isSetSetterExtensionDeclared) { // cannot be wrapped to nullable
model.imports.add("JsonDeserialize");
property.vendorExtensions.put("x-setter-extra-annotation", "@JsonDeserialize(as = LinkedHashSet.class)");
property.vendorExtensions.put(X_SETTER_EXTRA_ANNOTATION, "@JsonDeserialize(as = LinkedHashSet.class)");
}
} else if ("map".equals(property.containerType)) {
model.imports.add("HashMap");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,52 @@

package org.openapitools.codegen.java;

import java.io.File;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import com.google.common.collect.Sets;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.*;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.BooleanSchema;
import io.swagger.v3.oas.models.media.ByteArraySchema;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.DateTimeSchema;
import io.swagger.v3.oas.models.media.IntegerSchema;
import io.swagger.v3.oas.models.media.MapSchema;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.media.XML;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.QueryParameter;
import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
import io.swagger.v3.parser.util.SchemaTypeUtil;
import org.openapitools.codegen.*;
import org.openapitools.codegen.ClientOptInput;
import org.openapitools.codegen.CodegenConstants;
import org.openapitools.codegen.CodegenModel;
import org.openapitools.codegen.CodegenOperation;
import org.openapitools.codegen.CodegenParameter;
import org.openapitools.codegen.CodegenProperty;
import org.openapitools.codegen.CodegenResponse;
import org.openapitools.codegen.DefaultCodegen;
import org.openapitools.codegen.DefaultGenerator;
import org.openapitools.codegen.TestUtils;
import org.openapitools.codegen.config.CodegenConfigurator;
import org.openapitools.codegen.languages.JavaClientCodegen;
import org.openapitools.codegen.languages.features.DocumentationProviderFeatures.AnnotationLibrary;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.io.File;
import java.nio.file.Files;
import java.util.List;
import static org.openapitools.codegen.CodegenConstants.X_SETTER_EXTRA_ANNOTATION;

public class JavaModelTest {

Expand Down Expand Up @@ -171,6 +195,39 @@ public void setPropertyTest() {
Assert.assertTrue(property.isContainer);
}

@Test(description = "convert a model with set property and declared setter extension")
public void setPropertyWithDeclaredSetterExtensionTest() {
var declaredSetterExtension = "@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = org.openapitools.tools.TrimmingStringSetDeserializer.class)";
final Schema schema = new Schema()
.description("a sample model")
.addProperties("urls",
new ArraySchema()
.items(new StringSchema())
.uniqueItems(true)
.extensions(Map.of(X_SETTER_EXTRA_ANNOTATION, declaredSetterExtension))
)
.addRequiredItem("id");

final DefaultCodegen codegen = new DefaultCodegen();
OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema("sample", schema);
codegen.setOpenAPI(openAPI);

final CodegenModel cm = codegen.fromModel("sample", schema);
final CodegenProperty property = cm.vars.get(0);

Assert.assertTrue(
property.isSetSetterExtensionDeclared,
"Expected to be discovered if a setter annotation is declared"
);
Assert.assertListContains(
new ArrayList<>(property.getVendorExtensions().entrySet()),
extension ->
extension.getKey().equals(X_SETTER_EXTRA_ANNOTATION)
&& ((String) extension.getValue()).contains(declaredSetterExtension),
"Expected to have a setter extension if its annotation is declared"
);
}

@Test(description = "convert a model with a map property")
public void mapPropertyTest() {
final Schema schema = new Schema()
Expand Down