From 71ebeb22b80e257b9df5477cc592dc88c568550d Mon Sep 17 00:00:00 2001 From: adraarda23 Date: Thu, 7 May 2026 01:36:41 +0300 Subject: [PATCH 1/3] feat(spring): add x-inner-validation vendor extension for collection items Introduces a new field-level OpenAPI vendor extension `x-inner-validation` that places a custom annotation on the type argument of Java collections (`List`, `Set`) generated by the Spring server generator, enabling per-element bean validation constraints such as `@NotNull`. When set on the items schema, the generated POJO field, getter, setter and fluent builder all use the annotated parameterized type, e.g.: private List<@jakarta.validation.constraints.NotNull Stubb> sample; The implementation precomputes the full datatype string in `SpringCodegen#postProcessModelProperty` and exposes it via `x-datatype-with-inner-annotation` on the property; a small mustache partial (`dataTypeWithInnerAnnotation`) then renders that string or falls back to `{{datatypeWithEnum}}`, so existing models without the extension produce byte-identical output. The wrap is also propagated through `JsonNullable<...>` for nullable containers. Map / `additionalProperties` is intentionally not covered by this extension. Scoped to the Spring generator on purpose: the issue author's reference attempt was on `JavaSpring/pojo.mustache` and Spring has no library overrides for that template, so coverage is complete here. Other Java and JaxRS generators have library-specific pojo overrides that would each need to be wired up separately; that can be added in a follow-up if requested. Fixes #23705 --- docs/generators/java-camel.md | 1 + docs/generators/spring.md | 1 + .../openapitools/codegen/VendorExtension.java | 1 + .../codegen/languages/SpringCodegen.java | 16 +++++++ .../dataTypeWithInnerAnnotation.mustache | 1 + .../JavaSpring/nullableDataType.mustache | 2 +- .../nullableDataTypeBeanValidation.mustache | 2 +- .../main/resources/JavaSpring/pojo.mustache | 4 +- .../java/spring/SpringCodegenTest.java | 27 ++++++++++++ .../src/test/resources/3_0/issue_23705.yaml | 44 +++++++++++++++++++ 10 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 modules/openapi-generator/src/main/resources/JavaSpring/dataTypeWithInnerAnnotation.mustache create mode 100644 modules/openapi-generator/src/test/resources/3_0/issue_23705.yaml diff --git a/docs/generators/java-camel.md b/docs/generators/java-camel.md index 42c864eed0f8..3d9912dc6a0d 100644 --- a/docs/generators/java-camel.md +++ b/docs/generators/java-camel.md @@ -151,6 +151,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |x-minimum-message|Add this property whenever you need to customize the invalidation error message for the minimum value of a variable|FIELD, OPERATION_PARAMETER|null |x-maximum-message|Add this property whenever you need to customize the invalidation error message for the maximum value of a variable|FIELD, OPERATION_PARAMETER|null |x-spring-api-version|Value for 'version' attribute in @RequestMapping (for Spring 7 and above).|OPERATION|null +|x-inner-validation|Custom annotation to be placed on the type argument of a collection (List, Set), enabling per-element bean validation constraints (e.g. `@NotNull`)|FIELD|null ## IMPORT MAPPING diff --git a/docs/generators/spring.md b/docs/generators/spring.md index e1a4d7771e61..0675587ced11 100644 --- a/docs/generators/spring.md +++ b/docs/generators/spring.md @@ -144,6 +144,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |x-minimum-message|Add this property whenever you need to customize the invalidation error message for the minimum value of a variable|FIELD, OPERATION_PARAMETER|null |x-maximum-message|Add this property whenever you need to customize the invalidation error message for the maximum value of a variable|FIELD, OPERATION_PARAMETER|null |x-spring-api-version|Value for 'version' attribute in @RequestMapping (for Spring 7 and above).|OPERATION|null +|x-inner-validation|Custom annotation to be placed on the type argument of a collection (List, Set), enabling per-element bean validation constraints (e.g. `@NotNull`)|FIELD|null ## IMPORT MAPPING diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java index bce0e2a691f4..ce98dfd6faa1 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java @@ -24,6 +24,7 @@ public enum VendorExtension { X_CONTENT_TYPE("x-content-type", ExtensionLevel.OPERATION, "Specify custom value for 'Content-Type' header for operation", null), X_CLASS_EXTRA_ANNOTATION("x-class-extra-annotation", ExtensionLevel.MODEL, "List of custom annotations to be added to model", null), X_FIELD_EXTRA_ANNOTATION("x-field-extra-annotation", Arrays.asList(ExtensionLevel.FIELD, ExtensionLevel.OPERATION_PARAMETER), "List of custom annotations to be added to property", null), + X_INNER_VALIDATION("x-inner-validation", ExtensionLevel.FIELD, "Custom annotation to be placed on the type argument of a collection (List, Set), enabling per-element bean validation constraints (e.g. `@NotNull`)", null), X_OPERATION_EXTRA_ANNOTATION("x-operation-extra-annotation", ExtensionLevel.OPERATION, "List of custom annotations to be added to operation", null), X_VERSION_PARAM("x-version-param", ExtensionLevel.OPERATION_PARAMETER, "Marker property that tells that this parameter would be used for endpoint versioning. Applicable for headers & query params. true/false", null), X_PATTERN_MESSAGE("x-pattern-message", Arrays.asList(ExtensionLevel.FIELD, ExtensionLevel.OPERATION_PARAMETER), "Add this property whenever you need to customize the invalidation error message for the regex pattern of a variable", null), diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java index 9329e8a1b5c5..31ddb37346e2 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java @@ -1139,6 +1139,21 @@ public void setParameterExampleValue(CodegenParameter p) { public void postProcessModelProperty(CodegenModel model, CodegenProperty property) { super.postProcessModelProperty(model, property); + // x-inner-validation: when set on the items of an array/set, expose a precomputed + // datatype string that places the annotation as a JSR-308 type-use annotation on + // the element type, e.g. List<@NotNull Stubb> or Set<@NotNull Stubb>. + // Restricted to isArray (List/Set) — Maps are intentionally not supported. + if (property.isArray && property.items != null + && property.items.vendorExtensions != null + && property.items.vendorExtensions.get("x-inner-validation") instanceof String) { + String innerAnnotation = ((String) property.items.vendorExtensions.get("x-inner-validation")).trim(); + if (!innerAnnotation.isEmpty()) { + String containerType = property.getUniqueItems() ? "Set" : "List"; + String datatype = containerType + "<" + innerAnnotation + " " + property.items.datatypeWithEnum + ">"; + property.vendorExtensions.put("x-datatype-with-inner-annotation", datatype); + } + } + // add org.springframework.format.annotation.DateTimeFormat when needed if (property.isDate || property.isDateTime) { model.imports.add("DateTimeFormat"); @@ -1543,6 +1558,7 @@ public List getSupportedVendorExtensions() { extensions.add(VendorExtension.X_MINIMUM_MESSAGE); extensions.add(VendorExtension.X_MAXIMUM_MESSAGE); extensions.add(VendorExtension.X_SPRING_API_VERSION); + extensions.add(VendorExtension.X_INNER_VALIDATION); return extensions; } diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/dataTypeWithInnerAnnotation.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/dataTypeWithInnerAnnotation.mustache new file mode 100644 index 000000000000..030b944cf70e --- /dev/null +++ b/modules/openapi-generator/src/main/resources/JavaSpring/dataTypeWithInnerAnnotation.mustache @@ -0,0 +1 @@ +{{#vendorExtensions.x-datatype-with-inner-annotation}}{{{.}}}{{/vendorExtensions.x-datatype-with-inner-annotation}}{{^vendorExtensions.x-datatype-with-inner-annotation}}{{{datatypeWithEnum}}}{{/vendorExtensions.x-datatype-with-inner-annotation}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/nullableDataType.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/nullableDataType.mustache index cc1995172615..a844ec7af422 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/nullableDataType.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/nullableDataType.mustache @@ -1 +1 @@ -{{#lambda.trim}}{{#openApiNullable}}{{#isNullable}}JsonNullable<{{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}Optional<{{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{/openApiNullable}}{{#lambda.jSpecifyDatatype}}{{{datatypeWithEnum}}}{{/lambda.jSpecifyDatatype}}{{#openApiNullable}}{{#isNullable}}>{{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}>{{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{/openApiNullable}}{{/lambda.trim}} \ No newline at end of file +{{#lambda.trim}}{{#openApiNullable}}{{#isNullable}}JsonNullable<{{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}Optional<{{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{/openApiNullable}}{{#vendorExtensions.x-datatype-with-inner-annotation}}{{{.}}}{{/vendorExtensions.x-datatype-with-inner-annotation}}{{^vendorExtensions.x-datatype-with-inner-annotation}}{{#lambda.jSpecifyDatatype}}{{{datatypeWithEnum}}}{{/lambda.jSpecifyDatatype}}{{/vendorExtensions.x-datatype-with-inner-annotation}}{{#openApiNullable}}{{#isNullable}}>{{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}>{{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{/openApiNullable}}{{/lambda.trim}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/nullableDataTypeBeanValidation.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/nullableDataTypeBeanValidation.mustache index 808d1b6312af..92750f67e7c2 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/nullableDataTypeBeanValidation.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/nullableDataTypeBeanValidation.mustache @@ -1 +1 @@ -{{#lambda.trim}}{{#openApiNullable}}{{#isNullable}}{{^isContainer}}JsonNullable<{{#useBeanValidation}}{{>beanValidationCore}}{{/useBeanValidation}}{{/isContainer}}{{#isContainer}}JsonNullable<{{/isContainer}}{{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}Optional<{{#useBeanValidation}}{{>beanValidationCore}}{{/useBeanValidation}}{{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{/openApiNullable}}{{#lambda.jSpecifyDatatype}}{{{datatypeWithEnum}}}{{/lambda.jSpecifyDatatype}}{{#openApiNullable}}{{#isNullable}}>{{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}>{{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{/openApiNullable}}{{/lambda.trim}} \ No newline at end of file +{{#lambda.trim}}{{#openApiNullable}}{{#isNullable}}{{^isContainer}}JsonNullable<{{#useBeanValidation}}{{>beanValidationCore}}{{/useBeanValidation}}{{/isContainer}}{{#isContainer}}JsonNullable<{{/isContainer}}{{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}Optional<{{#useBeanValidation}}{{>beanValidationCore}}{{/useBeanValidation}}{{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{/openApiNullable}}{{#vendorExtensions.x-datatype-with-inner-annotation}}{{{.}}}{{/vendorExtensions.x-datatype-with-inner-annotation}}{{^vendorExtensions.x-datatype-with-inner-annotation}}{{#lambda.jSpecifyDatatype}}{{{datatypeWithEnum}}}{{/lambda.jSpecifyDatatype}}{{/vendorExtensions.x-datatype-with-inner-annotation}}{{#openApiNullable}}{{#isNullable}}>{{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}>{{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{/openApiNullable}}{{/lambda.trim}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache index f76cf70a7b59..b31d91a4f0fa 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache @@ -67,7 +67,7 @@ public {{>sealed}}class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}} {{#isContainer}} {{#useBeanValidation}}@Valid{{/useBeanValidation}} {{#openApiNullable}} - private {{>nullableAnnotation}}{{#isNullable}}{{>nullableDataTypeBeanValidation}} {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>undefined();{{/isNullable}}{{^required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/isNullable}}{{/required}}{{#required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/isNullable}}{{/required}} + private {{>nullableAnnotation}}{{#isNullable}}{{>nullableDataTypeBeanValidation}} {{name}} = JsonNullable.<{{>dataTypeWithInnerAnnotation}}>undefined();{{/isNullable}}{{^required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/isNullable}}{{/required}}{{#required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/isNullable}}{{/required}} {{/openApiNullable}} {{^openApiNullable}} private {{>nullableAnnotation}}{{>nullableDataType}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}; @@ -144,7 +144,7 @@ public {{>sealed}}class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}} {{^lombok.Data}} {{! begin feature: fluent setter methods }} - public {{classname}} {{name}}({{>nullableAnnotation}}{{{datatypeWithEnum}}} {{name}}) { + public {{classname}} {{name}}({{>nullableAnnotation}}{{>dataTypeWithInnerAnnotation}} {{name}}) { {{#openApiNullable}} this.{{name}} = {{#isNullable}}JsonNullable.of({{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}Optional.of{{#optionalAcceptNullable}}Nullable{{/optionalAcceptNullable}}({{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{name}}{{#isNullable}}){{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}){{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}; {{/openApiNullable}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java index da1e4aefface..271cd35be1dd 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java @@ -7725,4 +7725,31 @@ void disableDiscriminatorJsonIgnorePropertiesIsTrueThenJsonIgnorePropertiesShoul JavaFileAssert.assertThat(files.get("BaseConfiguration.java")) .assertTypeAnnotations().containsWithName("JsonIgnoreProperties"); } + + @Test + void innerValidationAnnotationOnCollectionItems_issue23705() throws IOException { + Map files = generateFromContract("src/test/resources/3_0/issue_23705.yaml", SPRING_BOOT, + Map.of("useBeanValidation", "true", "useSpringBoot3", "true")); + + JavaFileAssert.assertThat(files.get("SampleModel.java")) + // field declarations + .fileContains("private List<@jakarta.validation.constraints.NotNull Stubb> listSample") + .fileContains("private Set<@jakarta.validation.constraints.NotNull Stubb> setSample") + // getter return types + .fileContains("public List<@jakarta.validation.constraints.NotNull Stubb> getListSample()") + .fileContains("public Set<@jakarta.validation.constraints.NotNull Stubb> getSetSample()") + // setter parameter types + .fileContains("public void setListSample(List<@jakarta.validation.constraints.NotNull Stubb> listSample)") + .fileContains("public void setSetSample(Set<@jakarta.validation.constraints.NotNull Stubb> setSample)") + // fluent builder parameter types — the Set builder must use Set, not List + .fileContains("public SampleModel listSample(List<@jakarta.validation.constraints.NotNull Stubb> listSample)") + .fileContains("public SampleModel setSample(Set<@jakarta.validation.constraints.NotNull Stubb> setSample)") + // negative checks: the un-annotated container types should not appear for these fields + .fileDoesNotContain("List listSample") + .fileDoesNotContain("Set setSample") + // nullable container with inner annotation: JsonNullable wrap must keep the annotation + .fileContains("JsonNullable>") + // Map (additionalProperties) must NOT receive the inner annotation — only List/Set are supported + .fileDoesNotContain("Map"); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/issue_23705.yaml b/modules/openapi-generator/src/test/resources/3_0/issue_23705.yaml new file mode 100644 index 000000000000..9dbcfa8dbf48 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/issue_23705.yaml @@ -0,0 +1,44 @@ +openapi: 3.0.3 +info: + title: issue 23705 x-inner-validation + version: 1.0.0 +components: + schemas: + Stubb: + type: object + properties: + name: + type: string + SampleModel: + type: object + properties: + listSample: + type: array + items: + x-inner-validation: '@jakarta.validation.constraints.NotNull' + allOf: + - $ref: '#/components/schemas/Stubb' + setSample: + type: array + uniqueItems: true + items: + x-inner-validation: '@jakarta.validation.constraints.NotNull' + allOf: + - $ref: '#/components/schemas/Stubb' + regularList: + type: array + items: + $ref: '#/components/schemas/Stubb' + nullableListSample: + type: array + nullable: true + items: + x-inner-validation: '@jakarta.validation.constraints.NotNull' + allOf: + - $ref: '#/components/schemas/Stubb' + mapSample: + type: object + additionalProperties: + x-inner-validation: '@jakarta.validation.constraints.NotNull' + allOf: + - $ref: '#/components/schemas/Stubb' From 474c60c60e59f015700059abf91f3b0cf5439ab5 Mon Sep 17 00:00:00 2001 From: adraarda23 Date: Thu, 7 May 2026 03:48:31 +0300 Subject: [PATCH 2/3] docs(spring): clarify x-inner-validation is set on items schema Address cubic-dev-ai review (P2): the FIELD-level placement was misleading because Spring reads the extension from the array/set items schema, not directly from the property. --- docs/generators/java-camel.md | 2 +- docs/generators/spring.md | 2 +- .../src/main/java/org/openapitools/codegen/VendorExtension.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/generators/java-camel.md b/docs/generators/java-camel.md index 3d9912dc6a0d..53034faff1ad 100644 --- a/docs/generators/java-camel.md +++ b/docs/generators/java-camel.md @@ -151,7 +151,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |x-minimum-message|Add this property whenever you need to customize the invalidation error message for the minimum value of a variable|FIELD, OPERATION_PARAMETER|null |x-maximum-message|Add this property whenever you need to customize the invalidation error message for the maximum value of a variable|FIELD, OPERATION_PARAMETER|null |x-spring-api-version|Value for 'version' attribute in @RequestMapping (for Spring 7 and above).|OPERATION|null -|x-inner-validation|Custom annotation to be placed on the type argument of a collection (List, Set), enabling per-element bean validation constraints (e.g. `@NotNull`)|FIELD|null +|x-inner-validation|Custom annotation to be placed on the type argument of a collection (List, Set), set on the array/set `items` schema, enabling per-element bean validation constraints (e.g. `@NotNull`)|FIELD|null ## IMPORT MAPPING diff --git a/docs/generators/spring.md b/docs/generators/spring.md index 0675587ced11..5cc5049298e9 100644 --- a/docs/generators/spring.md +++ b/docs/generators/spring.md @@ -144,7 +144,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |x-minimum-message|Add this property whenever you need to customize the invalidation error message for the minimum value of a variable|FIELD, OPERATION_PARAMETER|null |x-maximum-message|Add this property whenever you need to customize the invalidation error message for the maximum value of a variable|FIELD, OPERATION_PARAMETER|null |x-spring-api-version|Value for 'version' attribute in @RequestMapping (for Spring 7 and above).|OPERATION|null -|x-inner-validation|Custom annotation to be placed on the type argument of a collection (List, Set), enabling per-element bean validation constraints (e.g. `@NotNull`)|FIELD|null +|x-inner-validation|Custom annotation to be placed on the type argument of a collection (List, Set), set on the array/set `items` schema, enabling per-element bean validation constraints (e.g. `@NotNull`)|FIELD|null ## IMPORT MAPPING diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java index ce98dfd6faa1..e267e845ba4a 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java @@ -24,7 +24,7 @@ public enum VendorExtension { X_CONTENT_TYPE("x-content-type", ExtensionLevel.OPERATION, "Specify custom value for 'Content-Type' header for operation", null), X_CLASS_EXTRA_ANNOTATION("x-class-extra-annotation", ExtensionLevel.MODEL, "List of custom annotations to be added to model", null), X_FIELD_EXTRA_ANNOTATION("x-field-extra-annotation", Arrays.asList(ExtensionLevel.FIELD, ExtensionLevel.OPERATION_PARAMETER), "List of custom annotations to be added to property", null), - X_INNER_VALIDATION("x-inner-validation", ExtensionLevel.FIELD, "Custom annotation to be placed on the type argument of a collection (List, Set), enabling per-element bean validation constraints (e.g. `@NotNull`)", null), + X_INNER_VALIDATION("x-inner-validation", ExtensionLevel.FIELD, "Custom annotation to be placed on the type argument of a collection (List, Set), set on the array/set `items` schema, enabling per-element bean validation constraints (e.g. `@NotNull`)", null), X_OPERATION_EXTRA_ANNOTATION("x-operation-extra-annotation", ExtensionLevel.OPERATION, "List of custom annotations to be added to operation", null), X_VERSION_PARAM("x-version-param", ExtensionLevel.OPERATION_PARAMETER, "Marker property that tells that this parameter would be used for endpoint versioning. Applicable for headers & query params. true/false", null), X_PATTERN_MESSAGE("x-pattern-message", Arrays.asList(ExtensionLevel.FIELD, ExtensionLevel.OPERATION_PARAMETER), "Add this property whenever you need to customize the invalidation error message for the regex pattern of a variable", null), From 277e3010ad32c0ee3b02c7cab78c84b238481082 Mon Sep 17 00:00:00 2001 From: adraarda23 Date: Fri, 8 May 2026 01:49:19 +0300 Subject: [PATCH 3/3] refactor(spring): rename x-inner-validation to x-item-validation per review feedback Move the extension from items.x-inner-validation (with allOf workaround) to the array property itself, and rename to x-item-validation per @jpfinne's PR review. The new syntax keeps $ref natural inside items and is more intuitive: listSample: type: array x-item-validation: '@jakarta.validation.constraints.NotNull' items: $ref: '#/components/schemas/Stubb' Internal mustache helpers (partial filename and precomputed extension key) also renamed for terminology consistency. --- docs/generators/java-camel.md | 2 +- docs/generators/spring.md | 2 +- .../openapitools/codegen/VendorExtension.java | 2 +- .../codegen/languages/SpringCodegen.java | 17 +++++++------- .../dataTypeWithInnerAnnotation.mustache | 1 - .../dataTypeWithItemAnnotation.mustache | 1 + .../JavaSpring/nullableDataType.mustache | 2 +- .../nullableDataTypeBeanValidation.mustache | 2 +- .../main/resources/JavaSpring/pojo.mustache | 4 ++-- .../java/spring/SpringCodegenTest.java | 6 ++--- .../src/test/resources/3_0/issue_23705.yaml | 22 ++++++++----------- 11 files changed, 28 insertions(+), 33 deletions(-) delete mode 100644 modules/openapi-generator/src/main/resources/JavaSpring/dataTypeWithInnerAnnotation.mustache create mode 100644 modules/openapi-generator/src/main/resources/JavaSpring/dataTypeWithItemAnnotation.mustache diff --git a/docs/generators/java-camel.md b/docs/generators/java-camel.md index 53034faff1ad..18ad84cb5a6f 100644 --- a/docs/generators/java-camel.md +++ b/docs/generators/java-camel.md @@ -151,7 +151,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |x-minimum-message|Add this property whenever you need to customize the invalidation error message for the minimum value of a variable|FIELD, OPERATION_PARAMETER|null |x-maximum-message|Add this property whenever you need to customize the invalidation error message for the maximum value of a variable|FIELD, OPERATION_PARAMETER|null |x-spring-api-version|Value for 'version' attribute in @RequestMapping (for Spring 7 and above).|OPERATION|null -|x-inner-validation|Custom annotation to be placed on the type argument of a collection (List, Set), set on the array/set `items` schema, enabling per-element bean validation constraints (e.g. `@NotNull`)|FIELD|null +|x-item-validation|Custom annotation to be placed on the type argument of a collection (List, Set), set on the array property itself, enabling per-element bean validation constraints (e.g. `@NotNull`)|FIELD|null ## IMPORT MAPPING diff --git a/docs/generators/spring.md b/docs/generators/spring.md index 5cc5049298e9..e8b5484a3cc6 100644 --- a/docs/generators/spring.md +++ b/docs/generators/spring.md @@ -144,7 +144,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |x-minimum-message|Add this property whenever you need to customize the invalidation error message for the minimum value of a variable|FIELD, OPERATION_PARAMETER|null |x-maximum-message|Add this property whenever you need to customize the invalidation error message for the maximum value of a variable|FIELD, OPERATION_PARAMETER|null |x-spring-api-version|Value for 'version' attribute in @RequestMapping (for Spring 7 and above).|OPERATION|null -|x-inner-validation|Custom annotation to be placed on the type argument of a collection (List, Set), set on the array/set `items` schema, enabling per-element bean validation constraints (e.g. `@NotNull`)|FIELD|null +|x-item-validation|Custom annotation to be placed on the type argument of a collection (List, Set), set on the array property itself, enabling per-element bean validation constraints (e.g. `@NotNull`)|FIELD|null ## IMPORT MAPPING diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java index e267e845ba4a..4f6801ebda50 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java @@ -24,7 +24,7 @@ public enum VendorExtension { X_CONTENT_TYPE("x-content-type", ExtensionLevel.OPERATION, "Specify custom value for 'Content-Type' header for operation", null), X_CLASS_EXTRA_ANNOTATION("x-class-extra-annotation", ExtensionLevel.MODEL, "List of custom annotations to be added to model", null), X_FIELD_EXTRA_ANNOTATION("x-field-extra-annotation", Arrays.asList(ExtensionLevel.FIELD, ExtensionLevel.OPERATION_PARAMETER), "List of custom annotations to be added to property", null), - X_INNER_VALIDATION("x-inner-validation", ExtensionLevel.FIELD, "Custom annotation to be placed on the type argument of a collection (List, Set), set on the array/set `items` schema, enabling per-element bean validation constraints (e.g. `@NotNull`)", null), + X_ITEM_VALIDATION("x-item-validation", ExtensionLevel.FIELD, "Custom annotation to be placed on the type argument of a collection (List, Set), set on the array property itself, enabling per-element bean validation constraints (e.g. `@NotNull`)", null), X_OPERATION_EXTRA_ANNOTATION("x-operation-extra-annotation", ExtensionLevel.OPERATION, "List of custom annotations to be added to operation", null), X_VERSION_PARAM("x-version-param", ExtensionLevel.OPERATION_PARAMETER, "Marker property that tells that this parameter would be used for endpoint versioning. Applicable for headers & query params. true/false", null), X_PATTERN_MESSAGE("x-pattern-message", Arrays.asList(ExtensionLevel.FIELD, ExtensionLevel.OPERATION_PARAMETER), "Add this property whenever you need to customize the invalidation error message for the regex pattern of a variable", null), diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java index 31ddb37346e2..5e23961fc910 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java @@ -1139,18 +1139,17 @@ public void setParameterExampleValue(CodegenParameter p) { public void postProcessModelProperty(CodegenModel model, CodegenProperty property) { super.postProcessModelProperty(model, property); - // x-inner-validation: when set on the items of an array/set, expose a precomputed + // x-item-validation: when set on an array/set property, expose a precomputed // datatype string that places the annotation as a JSR-308 type-use annotation on // the element type, e.g. List<@NotNull Stubb> or Set<@NotNull Stubb>. // Restricted to isArray (List/Set) — Maps are intentionally not supported. - if (property.isArray && property.items != null - && property.items.vendorExtensions != null - && property.items.vendorExtensions.get("x-inner-validation") instanceof String) { - String innerAnnotation = ((String) property.items.vendorExtensions.get("x-inner-validation")).trim(); - if (!innerAnnotation.isEmpty()) { + if (property.isArray && property.vendorExtensions != null + && property.vendorExtensions.get("x-item-validation") instanceof String) { + String itemAnnotation = ((String) property.vendorExtensions.get("x-item-validation")).trim(); + if (!itemAnnotation.isEmpty() && property.items != null) { String containerType = property.getUniqueItems() ? "Set" : "List"; - String datatype = containerType + "<" + innerAnnotation + " " + property.items.datatypeWithEnum + ">"; - property.vendorExtensions.put("x-datatype-with-inner-annotation", datatype); + String datatype = containerType + "<" + itemAnnotation + " " + property.items.datatypeWithEnum + ">"; + property.vendorExtensions.put("x-datatype-with-item-annotation", datatype); } } @@ -1558,7 +1557,7 @@ public List getSupportedVendorExtensions() { extensions.add(VendorExtension.X_MINIMUM_MESSAGE); extensions.add(VendorExtension.X_MAXIMUM_MESSAGE); extensions.add(VendorExtension.X_SPRING_API_VERSION); - extensions.add(VendorExtension.X_INNER_VALIDATION); + extensions.add(VendorExtension.X_ITEM_VALIDATION); return extensions; } diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/dataTypeWithInnerAnnotation.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/dataTypeWithInnerAnnotation.mustache deleted file mode 100644 index 030b944cf70e..000000000000 --- a/modules/openapi-generator/src/main/resources/JavaSpring/dataTypeWithInnerAnnotation.mustache +++ /dev/null @@ -1 +0,0 @@ -{{#vendorExtensions.x-datatype-with-inner-annotation}}{{{.}}}{{/vendorExtensions.x-datatype-with-inner-annotation}}{{^vendorExtensions.x-datatype-with-inner-annotation}}{{{datatypeWithEnum}}}{{/vendorExtensions.x-datatype-with-inner-annotation}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/dataTypeWithItemAnnotation.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/dataTypeWithItemAnnotation.mustache new file mode 100644 index 000000000000..adaa6ee4f4d4 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/JavaSpring/dataTypeWithItemAnnotation.mustache @@ -0,0 +1 @@ +{{#vendorExtensions.x-datatype-with-item-annotation}}{{{.}}}{{/vendorExtensions.x-datatype-with-item-annotation}}{{^vendorExtensions.x-datatype-with-item-annotation}}{{{datatypeWithEnum}}}{{/vendorExtensions.x-datatype-with-item-annotation}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/nullableDataType.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/nullableDataType.mustache index a844ec7af422..1391243bb833 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/nullableDataType.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/nullableDataType.mustache @@ -1 +1 @@ -{{#lambda.trim}}{{#openApiNullable}}{{#isNullable}}JsonNullable<{{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}Optional<{{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{/openApiNullable}}{{#vendorExtensions.x-datatype-with-inner-annotation}}{{{.}}}{{/vendorExtensions.x-datatype-with-inner-annotation}}{{^vendorExtensions.x-datatype-with-inner-annotation}}{{#lambda.jSpecifyDatatype}}{{{datatypeWithEnum}}}{{/lambda.jSpecifyDatatype}}{{/vendorExtensions.x-datatype-with-inner-annotation}}{{#openApiNullable}}{{#isNullable}}>{{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}>{{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{/openApiNullable}}{{/lambda.trim}} \ No newline at end of file +{{#lambda.trim}}{{#openApiNullable}}{{#isNullable}}JsonNullable<{{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}Optional<{{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{/openApiNullable}}{{#vendorExtensions.x-datatype-with-item-annotation}}{{{.}}}{{/vendorExtensions.x-datatype-with-item-annotation}}{{^vendorExtensions.x-datatype-with-item-annotation}}{{#lambda.jSpecifyDatatype}}{{{datatypeWithEnum}}}{{/lambda.jSpecifyDatatype}}{{/vendorExtensions.x-datatype-with-item-annotation}}{{#openApiNullable}}{{#isNullable}}>{{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}>{{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{/openApiNullable}}{{/lambda.trim}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/nullableDataTypeBeanValidation.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/nullableDataTypeBeanValidation.mustache index 92750f67e7c2..e8a0135237dc 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/nullableDataTypeBeanValidation.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/nullableDataTypeBeanValidation.mustache @@ -1 +1 @@ -{{#lambda.trim}}{{#openApiNullable}}{{#isNullable}}{{^isContainer}}JsonNullable<{{#useBeanValidation}}{{>beanValidationCore}}{{/useBeanValidation}}{{/isContainer}}{{#isContainer}}JsonNullable<{{/isContainer}}{{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}Optional<{{#useBeanValidation}}{{>beanValidationCore}}{{/useBeanValidation}}{{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{/openApiNullable}}{{#vendorExtensions.x-datatype-with-inner-annotation}}{{{.}}}{{/vendorExtensions.x-datatype-with-inner-annotation}}{{^vendorExtensions.x-datatype-with-inner-annotation}}{{#lambda.jSpecifyDatatype}}{{{datatypeWithEnum}}}{{/lambda.jSpecifyDatatype}}{{/vendorExtensions.x-datatype-with-inner-annotation}}{{#openApiNullable}}{{#isNullable}}>{{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}>{{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{/openApiNullable}}{{/lambda.trim}} \ No newline at end of file +{{#lambda.trim}}{{#openApiNullable}}{{#isNullable}}{{^isContainer}}JsonNullable<{{#useBeanValidation}}{{>beanValidationCore}}{{/useBeanValidation}}{{/isContainer}}{{#isContainer}}JsonNullable<{{/isContainer}}{{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}Optional<{{#useBeanValidation}}{{>beanValidationCore}}{{/useBeanValidation}}{{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{/openApiNullable}}{{#vendorExtensions.x-datatype-with-item-annotation}}{{{.}}}{{/vendorExtensions.x-datatype-with-item-annotation}}{{^vendorExtensions.x-datatype-with-item-annotation}}{{#lambda.jSpecifyDatatype}}{{{datatypeWithEnum}}}{{/lambda.jSpecifyDatatype}}{{/vendorExtensions.x-datatype-with-item-annotation}}{{#openApiNullable}}{{#isNullable}}>{{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}>{{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{/openApiNullable}}{{/lambda.trim}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache index b31d91a4f0fa..ece4ee0500e9 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache @@ -67,7 +67,7 @@ public {{>sealed}}class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}} {{#isContainer}} {{#useBeanValidation}}@Valid{{/useBeanValidation}} {{#openApiNullable}} - private {{>nullableAnnotation}}{{#isNullable}}{{>nullableDataTypeBeanValidation}} {{name}} = JsonNullable.<{{>dataTypeWithInnerAnnotation}}>undefined();{{/isNullable}}{{^required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/isNullable}}{{/required}}{{#required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/isNullable}}{{/required}} + private {{>nullableAnnotation}}{{#isNullable}}{{>nullableDataTypeBeanValidation}} {{name}} = JsonNullable.<{{>dataTypeWithItemAnnotation}}>undefined();{{/isNullable}}{{^required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/isNullable}}{{/required}}{{#required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/isNullable}}{{/required}} {{/openApiNullable}} {{^openApiNullable}} private {{>nullableAnnotation}}{{>nullableDataType}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}; @@ -144,7 +144,7 @@ public {{>sealed}}class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}} {{^lombok.Data}} {{! begin feature: fluent setter methods }} - public {{classname}} {{name}}({{>nullableAnnotation}}{{>dataTypeWithInnerAnnotation}} {{name}}) { + public {{classname}} {{name}}({{>nullableAnnotation}}{{>dataTypeWithItemAnnotation}} {{name}}) { {{#openApiNullable}} this.{{name}} = {{#isNullable}}JsonNullable.of({{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}Optional.of{{#optionalAcceptNullable}}Nullable{{/optionalAcceptNullable}}({{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{name}}{{#isNullable}}){{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}){{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}; {{/openApiNullable}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java index 271cd35be1dd..4ce5c8d3df4d 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java @@ -7727,7 +7727,7 @@ void disableDiscriminatorJsonIgnorePropertiesIsTrueThenJsonIgnorePropertiesShoul } @Test - void innerValidationAnnotationOnCollectionItems_issue23705() throws IOException { + void itemValidationAnnotationOnCollectionItems_issue23705() throws IOException { Map files = generateFromContract("src/test/resources/3_0/issue_23705.yaml", SPRING_BOOT, Map.of("useBeanValidation", "true", "useSpringBoot3", "true")); @@ -7747,9 +7747,9 @@ void innerValidationAnnotationOnCollectionItems_issue23705() throws IOException // negative checks: the un-annotated container types should not appear for these fields .fileDoesNotContain("List listSample") .fileDoesNotContain("Set setSample") - // nullable container with inner annotation: JsonNullable wrap must keep the annotation + // nullable container with item annotation: JsonNullable wrap must keep the annotation .fileContains("JsonNullable>") - // Map (additionalProperties) must NOT receive the inner annotation — only List/Set are supported + // Map (additionalProperties) must NOT receive the item annotation — only List/Set are supported .fileDoesNotContain("Map"); } } diff --git a/modules/openapi-generator/src/test/resources/3_0/issue_23705.yaml b/modules/openapi-generator/src/test/resources/3_0/issue_23705.yaml index 9dbcfa8dbf48..6390e999d3ab 100644 --- a/modules/openapi-generator/src/test/resources/3_0/issue_23705.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/issue_23705.yaml @@ -1,6 +1,6 @@ openapi: 3.0.3 info: - title: issue 23705 x-inner-validation + title: issue 23705 x-item-validation version: 1.0.0 components: schemas: @@ -14,17 +14,15 @@ components: properties: listSample: type: array + x-item-validation: '@jakarta.validation.constraints.NotNull' items: - x-inner-validation: '@jakarta.validation.constraints.NotNull' - allOf: - - $ref: '#/components/schemas/Stubb' + $ref: '#/components/schemas/Stubb' setSample: type: array uniqueItems: true + x-item-validation: '@jakarta.validation.constraints.NotNull' items: - x-inner-validation: '@jakarta.validation.constraints.NotNull' - allOf: - - $ref: '#/components/schemas/Stubb' + $ref: '#/components/schemas/Stubb' regularList: type: array items: @@ -32,13 +30,11 @@ components: nullableListSample: type: array nullable: true + x-item-validation: '@jakarta.validation.constraints.NotNull' items: - x-inner-validation: '@jakarta.validation.constraints.NotNull' - allOf: - - $ref: '#/components/schemas/Stubb' + $ref: '#/components/schemas/Stubb' mapSample: type: object + x-item-validation: '@jakarta.validation.constraints.NotNull' additionalProperties: - x-inner-validation: '@jakarta.validation.constraints.NotNull' - allOf: - - $ref: '#/components/schemas/Stubb' + $ref: '#/components/schemas/Stubb'