Skip to content

Commit 3dbc3f6

Browse files
committed
fix(java): place bean-validation @Valid on the type argument instead of the container (HV000271)
Hibernate Validator 9.1+ (bundled with Spring Boot 4) logs HV000271 ("Using @Valid on a container is deprecated. You should apply the annotation on the type argument(s)") for every @Valid-annotated List/Set/Map — on both model properties and API parameters. This relocates @Valid from the container to its type argument — the Bean-Validation-2.0 form (List<@Valid T>, Map<String, @Valid V>) — which cascades identically but is not deprecated. Backward-compatible: element validation is preserved, never dropped; single-object @Valid is untouched. Verified on Spring Boot 3.3 (Spring 6.1) and 4.1 (Spring 7): a container parameter carrying only the type-argument @Valid still triggers element validation via method validation, with no HV000271. Scope (Java family): spring, java client, JAX-RS (jersey, resteasy(+eap), cxf(+extended/cdi), spec), java-camel, java-msf4j. Models (property/getter): the container @Valid is moved to the type argument. Map values are gated behind a new AbstractJavaCodegen#useBeanValidationOnMapValueType() (default false; overridden true in Spring/JavaClient/JAX-RS) so untouched generators don't silently gain map-value validation. Arrays/sets already injected the type-argument form. oneOf/anyOf/allOf model elements now also receive the type-argument @Valid (List<@Valid ShapeOneOf>), so their element validation is preserved rather than dropped; a oneOf of constants (an enum) stays without @Valid. Parameters (@RequestBody/@RequestParam/@RequestPart): the redundant container-level @Valid is dropped for container parameters; element validation is driven by the type-argument @Valid. Single-object parameters keep @Valid. An Optional-wrapped scalar parameter (useOptional, e.g. Optional<Integer>) no longer emits a parameter-level @Valid either — Hibernate Validator treats Optional as a container, so it was the same HV000271 with nothing to cascade into. Reactive Mono/Flux bodies are intentionally untouched (not Jakarta containers, so no HV000271). Regenerated all affected samples and added/updated codegen tests in SpringCodegenTest, JavaClientCodegenTest, JavaJAXRSSpecServerCodegenTest and JavaValidationArrayPrimitivesTest.
1 parent 08919b2 commit 3dbc3f6

598 files changed

Lines changed: 1476 additions & 1437 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,7 +1099,7 @@ public String getTypeDeclaration(Schema p) {
10991099
Schema<?> target = ModelUtils.isGenerateAliasAsModel() ? p : schema;
11001100
if (ModelUtils.isArraySchema(target)) {
11011101
Schema<?> items = getSchemaItems(schema);
1102-
String typeDeclaration = getTypeDeclarationForArray(items);
1102+
String typeDeclaration = getTypeDeclarationWithBeanValidation(items);
11031103
return getSchemaType(target) + "<" + typeDeclaration + ">";
11041104
} else if (ModelUtils.isMapSchema(target)) {
11051105
// Note: ModelUtils.isMapSchema(p) returns true when p is a composed schema that also defines
@@ -1110,12 +1110,43 @@ public String getTypeDeclaration(Schema p) {
11101110
inner = new StringSchema().description("TODO default missing map inner type to string");
11111111
p.setAdditionalProperties(inner);
11121112
}
1113-
return getSchemaType(target) + "<String, " + getTypeDeclaration(inner) + ">";
1113+
// Unlike arrays/sets, map values never received a type-argument bean
1114+
// validation before, so this is gated: only generators that have dropped the
1115+
// deprecated container-level @Valid (HV000271) opt in, to avoid silently adding
1116+
// new validation to generators that still cascade via the container.
1117+
String valueDeclaration = useBeanValidationOnMapValueType()
1118+
? getTypeDeclarationWithBeanValidation(inner)
1119+
: getTypeDeclaration(inner);
1120+
return getSchemaType(target) + "<String, " + valueDeclaration + ">";
11141121
}
11151122
return super.getTypeDeclaration(target);
11161123
}
11171124

1118-
private String getTypeDeclarationForArray(Schema<?> items) {
1125+
/**
1126+
* Whether bean validation of map values is expressed on the value type argument
1127+
* ({@code Map<String, @Valid V>}) instead of on the map itself. Generators that have migrated
1128+
* off the deprecated container-level {@code @Valid} (Hibernate Validator HV000271)
1129+
* override this to return {@code true}. Arrays/sets always place bean validation on the
1130+
* type argument, so they are not gated by this method.
1131+
*
1132+
* @return {@code true} to emit map-value bean validation on the type argument;
1133+
* {@code false} by default
1134+
*/
1135+
protected boolean useBeanValidationOnMapValueType() {
1136+
return false;
1137+
}
1138+
1139+
/**
1140+
* Renders the type declaration of a container element (array/set item or map value) with its
1141+
* bean validation applied to the type argument, e.g. {@code @Valid Pet} or
1142+
* {@code @Size(max = 3) String}. Hibernate Validator 9.1+ expects cascade/constraints on the
1143+
* type argument; a container-level {@code @Valid} is deprecated and logs HV000271, so the
1144+
* annotation is placed here instead of on the container.
1145+
*
1146+
* @param items the array/set item or map value schema
1147+
* @return the element type declaration prefixed with its bean validation (no prefix if none)
1148+
*/
1149+
private String getTypeDeclarationWithBeanValidation(Schema<?> items) {
11191150
String typeDeclaration = getTypeDeclaration(items);
11201151

11211152
String beanValidation = getBeanValidation(items);
@@ -1139,11 +1170,12 @@ private String getTypeDeclarationForArray(Schema<?> items) {
11391170
}
11401171

11411172
/**
1142-
* This method stand for resolve bean validation for container(array, set).
1173+
* This method stand for resolve bean validation for a container element
1174+
* (array/set item or map value).
11431175
* Return empty if there's no bean validation for requested type or prop useBeanValidation false or missed.
11441176
*
11451177
* @param items type
1146-
* @return BeanValidation for declared type in container(array, set)
1178+
* @return BeanValidation for declared element type of a container (array, set, map value)
11471179
*/
11481180
private String getBeanValidation(Schema<?> items) {
11491181
if (!isUseBeanValidation()) {
@@ -1160,7 +1192,10 @@ private String getBeanValidation(Schema<?> items) {
11601192
String ref = ModelUtils.getSimpleRef(items.get$ref());
11611193
if (ref != null) {
11621194
Schema<?> schema = schemas.get(ref);
1163-
if (schema == null || ModelUtils.isObjectSchema(schema)) {
1195+
if (schema == null || ModelUtils.isObjectSchema(schema)
1196+
|| (ModelUtils.isComposedSchema(schema) && !ModelUtils.isOneOfOfConsts(schema))) {
1197+
// objects and oneOf/anyOf/allOf models cascade validation into their elements;
1198+
// a oneOf of constants generates an enum, which is not cascadable.
11641199
return "@Valid ";
11651200
}
11661201
items = schema;

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaJAXRSServerCodegen.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,14 @@ public CodegenType getTag() {
132132
return CodegenType.SERVER;
133133
}
134134

135+
@Override
136+
protected boolean useBeanValidationOnMapValueType() {
137+
// The JAX-RS templates place container element validation on the type argument
138+
// (List<@Valid T>, Map<String, @Valid V>) rather than the deprecated
139+
// container-level @Valid (HV000271).
140+
return true;
141+
}
142+
135143
@Override
136144
public void processOpts() {
137145
super.processOpts();

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,14 @@ public DocumentationProvider defaultDocumentationProvider() {
206206
return DocumentationProvider.SOURCE;
207207
}
208208

209+
@Override
210+
protected boolean useBeanValidationOnMapValueType() {
211+
// The Java templates place container element validation on the type argument
212+
// (List<@Valid T>, Map<String, @Valid V>) rather than the deprecated
213+
// container-level @Valid (HV000271).
214+
return true;
215+
}
216+
209217
@Override
210218
public List<DocumentationProvider> supportedDocumentationProvider() {
211219
List<DocumentationProvider> documentationProviders = new ArrayList<>();

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1734,6 +1734,14 @@ public List<VendorExtension> getSupportedVendorExtensions() {
17341734
return extensions;
17351735
}
17361736

1737+
@Override
1738+
protected boolean useBeanValidationOnMapValueType() {
1739+
// The Spring templates place container element validation on the type argument
1740+
// (List<@Valid T>, Map<String, @Valid V>) rather than the deprecated
1741+
// container-level @Valid (HV000271).
1742+
return true;
1743+
}
1744+
17371745
protected boolean isSpringCodegen() {
17381746
return getName().contains("spring");
17391747
}

modules/openapi-generator/src/main/resources/Java/beanValidation.mustache

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,6 @@
33
@NotNull
44
{{/isReadOnly}}
55
{{/required}}
6-
{{#isContainer}}
7-
{{^isPrimitiveType}}
8-
{{^isEnum}}
9-
@Valid
10-
{{/isEnum}}
11-
{{/isPrimitiveType}}
12-
{{/isContainer}}
136
{{^isContainer}}
147
{{^isPrimitiveType}}
158
@Valid
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{{#isBodyParam}}{{#useBeanValidation}}@Valid {{/useBeanValidation}}{{{dataType}}} {{paramName}}{{/isBodyParam}}
1+
{{#isBodyParam}}{{#useBeanValidation}}{{^isContainer}}@Valid {{/isContainer}}{{/useBeanValidation}}{{{dataType}}} {{paramName}}{{/isBodyParam}}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{{#required}}{{^isReadOnly}}@NotNull {{/isReadOnly}}{{/required}}{{#isContainer}}{{^items.isPrimitiveType}}{{^items.isDate}}{{^items.isDateTime}}{{^items.isString}}{{^items.isFile}}{{^items.isEnumOrRef}}@Valid {{/items.isEnumOrRef}}{{/items.isFile}}{{/items.isString}}{{/items.isDateTime}}{{/items.isDate}}{{/items.isPrimitiveType}}{{/isContainer}}{{^isContainer}}{{^isPrimitiveType}}{{^isDate}}{{^isDateTime}}{{^isString}}{{^isFile}}{{^isEnumOrRef}}@Valid {{/isEnumOrRef}}{{/isFile}}{{/isString}}{{/isDateTime}}{{/isDate}}{{/isPrimitiveType}}{{/isContainer}}{{>beanValidationCore}}
1+
{{#required}}{{^isReadOnly}}@NotNull {{/isReadOnly}}{{/required}}{{^isContainer}}{{^isPrimitiveType}}{{^isDate}}{{^isDateTime}}{{^isString}}{{^isFile}}{{^isEnumOrRef}}@Valid {{/isEnumOrRef}}{{/isFile}}{{/isString}}{{/isDateTime}}{{/isDate}}{{/isPrimitiveType}}{{/isContainer}}{{>beanValidationCore}}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{{#isBodyParam}}{{#useBeanValidation}}@Valid {{/useBeanValidation}}{{{dataType}}} {{paramName}}{{/isBodyParam}}
1+
{{#isBodyParam}}{{#useBeanValidation}}{{^isContainer}}@Valid {{/isContainer}}{{/useBeanValidation}}{{{dataType}}} {{paramName}}{{/isBodyParam}}

modules/openapi-generator/src/main/resources/JavaJaxRS/cxf-ext/pojo.mustache

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ public class {{classname}} {{#parent}}extends {{{.}}}{{/parent}}{{#vendorExtensi
4040
{{#withXml}}
4141
@XmlElement(name="{{baseName}}"{{#required}}, required = {{required}}{{/required}})
4242
{{/withXml}}
43-
@ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}value = "{{{description}}}"){{^isPrimitiveType}}{{^isDate}}{{^isDateTime}}{{^isString}}{{^isFile}}{{#useBeanValidation}}
44-
@Valid{{/useBeanValidation}}{{/isFile}}{{/isString}}{{/isDateTime}}{{/isDate}}{{/isPrimitiveType}}
43+
@ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}value = "{{{description}}}"){{^isContainer}}{{^isPrimitiveType}}{{^isDate}}{{^isDateTime}}{{^isString}}{{^isFile}}{{#useBeanValidation}}
44+
@Valid{{/useBeanValidation}}{{/isFile}}{{/isString}}{{/isDateTime}}{{/isDate}}{{/isPrimitiveType}}{{/isContainer}}
4545
{{#isDate}}
4646
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
4747
{{/isDate}}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{{#isBodyParam}}{{#useBeanValidation}}@Valid {{#required}}{{^isNullable}}@NotNull {{/isNullable}}{{/required}}{{/useBeanValidation}}{{#isFile}}{{#useAbstractionForFiles}}{{#collectionFormat}}java.util.Collection<InputStream>{{/collectionFormat}}{{^collectionFormat}}InputStream{{/collectionFormat}}{{/useAbstractionForFiles}}{{^useAbstractionForFiles}}{{{dataType}}}{{/useAbstractionForFiles}}{{/isFile}}{{^isFile}}{{{dataType}}}{{/isFile}} {{paramName}}{{/isBodyParam}}
1+
{{#isBodyParam}}{{#useBeanValidation}}{{^isContainer}}@Valid {{/isContainer}}{{#required}}{{^isNullable}}@NotNull {{/isNullable}}{{/required}}{{/useBeanValidation}}{{#isFile}}{{#useAbstractionForFiles}}{{#collectionFormat}}java.util.Collection<InputStream>{{/collectionFormat}}{{^collectionFormat}}InputStream{{/collectionFormat}}{{/useAbstractionForFiles}}{{^useAbstractionForFiles}}{{{dataType}}}{{/useAbstractionForFiles}}{{/isFile}}{{^isFile}}{{{dataType}}}{{/isFile}} {{paramName}}{{/isBodyParam}}

0 commit comments

Comments
 (0)