Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions docs/generators/java-camel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/generators/spring.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated


## IMPORT MAPPING
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -1543,6 +1558,7 @@ public List<VendorExtension> 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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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}}
Original file line number Diff line number Diff line change
@@ -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}}
{{#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}}
Original file line number Diff line number Diff line change
@@ -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}}
{{#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}}
Original file line number Diff line number Diff line change
Expand Up @@ -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}};
Expand Down Expand Up @@ -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}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7725,4 +7725,31 @@ void disableDiscriminatorJsonIgnorePropertiesIsTrueThenJsonIgnorePropertiesShoul
JavaFileAssert.assertThat(files.get("BaseConfiguration.java"))
.assertTypeAnnotations().containsWithName("JsonIgnoreProperties");
}

@Test
void innerValidationAnnotationOnCollectionItems_issue23705() throws IOException {
Map<String, File> 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<Stubb> listSample")
.fileDoesNotContain("Set<Stubb> setSample")
// nullable container with inner annotation: JsonNullable wrap must keep the annotation
.fileContains("JsonNullable<List<@jakarta.validation.constraints.NotNull Stubb>>")
// Map (additionalProperties) must NOT receive the inner annotation — only List/Set are supported
.fileDoesNotContain("Map<String, @jakarta.validation.constraints.NotNull Stubb>");
}
}
44 changes: 44 additions & 0 deletions modules/openapi-generator/src/test/resources/3_0/issue_23705.yaml
Original file line number Diff line number Diff line change
@@ -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'