diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java index ad33493416cd..b7a0fe4b23e6 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java @@ -2948,14 +2948,109 @@ protected ImmutableMap.Builder addMustacheLambdas() { }; Mustache.Lambda javaStringLiteralLambda = (fragment, writer) -> writer.write(toEnumValue(fragment.execute(), "String")); + // Places type-use annotations rendered in front of a (possibly fully qualified) type where Java + // allows them, e.g. {{#lambda.typeUseAnnotations}}Optional<@Valid {{{dataType}}}{{/lambda.typeUseAnnotations}} + // -> Optional<@Valid Pet> or Optional (Optional<@Valid java.time.Instant> does not compile). + Mustache.Lambda typeUseAnnotationsLambda = (fragment, writer) -> + writer.write(placeTypeUseAnnotations(fragment.execute())); return super.addMustacheLambdas() .put("javaStringLiteral", javaStringLiteralLambda) + .put("typeUseAnnotations", typeUseAnnotationsLambda) .put("jSpecifyDatatype", jSpecifyDatatypeLambda) .put("jSpecifyNullable", jSpecifyNullableLambda) .put("escapeJavaDoc", new EscapeJavaDocLambda()); } + /** + * Moves the type-use annotations written in front of the innermost type argument of {@code declaration} after the + * package qualifier when that type is fully qualified, as required by the Java grammar: + * {@code Optional<@Valid java.time.Instant>} becomes {@code Optional}. Declarations whose + * annotated type is not qualified, or that carry no annotations, are returned unchanged. + * + * @param declaration rendered type declaration, e.g. {@code JsonNullable<@Valid @Size(max = 3) com.acme.Code} + * @return the declaration with its type-use annotations at a legal position + */ + public static String placeTypeUseAnnotations(String declaration) { + int firstAnnotation = declaration.indexOf('@'); + if (firstAnnotation < 0) { + return declaration; + } + // keep the wrapper (e.g. "Optional<") untouched; the annotations apply to the type argument that follows + int start = declaration.lastIndexOf('<', firstAnnotation) + 1; + if (!StringUtils.isBlank(declaration.substring(start, firstAnnotation))) { + // something else precedes the first annotation (e.g. "java.time.@Nullable Instant"): already placed + return declaration; + } + int end = skipAnnotations(declaration, firstAnnotation); + String annotations = declaration.substring(firstAnnotation, end); + String type = declaration.substring(end); + int idx = getLastIndexOfQualifier(type); + if (idx <= 0) { + return declaration; + } + return declaration.substring(0, firstAnnotation) + type.substring(0, idx + 1) + annotations + type.substring(idx + 1); + } + + /** + * @return the index right after the annotations (and their trailing whitespace) starting at {@code index} + */ + private static int skipAnnotations(String declaration, int index) { + int i = index; + int length = declaration.length(); + while (i < length && declaration.charAt(i) == '@') { + i++; + while (i < length && (Character.isJavaIdentifierPart(declaration.charAt(i)) || declaration.charAt(i) == '.')) { + i++; + } + if (i < length && declaration.charAt(i) == '(') { + i = skipParentheses(declaration, i); + } + while (i < length && Character.isWhitespace(declaration.charAt(i))) { + i++; + } + } + return i; + } + + /** + * @return the index right after the parenthesis group opened at {@code index}, ignoring parentheses in string literals + */ + private static int skipParentheses(String declaration, int index) { + int depth = 0; + boolean inString = false; + for (int i = index; i < declaration.length(); i++) { + char c = declaration.charAt(i); + if (inString) { + if (c == '\\') { + i++; + } else if (c == '"') { + inString = false; + } + } else if (c == '"') { + inString = true; + } else if (c == '(') { + depth++; + } else if (c == ')' && --depth == 0) { + return i + 1; + } + } + return declaration.length(); + } + + private static int getLastIndexOfQualifier(String dataType) { + int index = dataType.indexOf('<'); + if (index >= 0) { + dataType = dataType.substring(0, index); + } + int at = dataType.indexOf('@'); + if (at >= 0) { + // e.g. "java.time.@Nullable Instant": the qualifier ends before the existing annotation + dataType = dataType.substring(0, at); + } + return dataType.lastIndexOf('.'); + } + private int getLastIndex(String dataType) { int index = dataType.indexOf('<'); if (index >= 0) { diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/beanValidation.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/beanValidation.mustache index 588458325006..4b156d89efc5 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/beanValidation.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/beanValidation.mustache @@ -1 +1 @@ -{{>notNull}}{{^isContainer}}{{^isPrimitiveType}}@Valid {{/isPrimitiveType}}{{/isContainer}}{{^openApiNullable}}{{>beanValidationCore}}{{/openApiNullable}}{{#openApiNullable}}{{^useOptional}}{{>beanValidationCore}}{{/useOptional}}{{/openApiNullable}}{{#useOptional}}{{#openApiNullable}}{{#isContainer}}{{^required}}{{>beanValidationCore}}{{/required}}{{/isContainer}}{{/openApiNullable}}{{#openApiNullable}}{{#required}}{{>beanValidationCore}}{{/required}}{{/openApiNullable}}{{/useOptional}} \ No newline at end of file +{{>notNull}}{{^isContainer}}{{^isPrimitiveType}}{{^openApiNullable}}@Valid {{/openApiNullable}}{{#openApiNullable}}{{^isNullable}}{{^useOptional}}@Valid {{/useOptional}}{{#useOptional}}{{#required}}@Valid {{/required}}{{/useOptional}}{{/isNullable}}{{/openApiNullable}}{{/isPrimitiveType}}{{/isContainer}}{{^openApiNullable}}{{>beanValidationCore}}{{/openApiNullable}}{{#openApiNullable}}{{^useOptional}}{{>beanValidationCore}}{{/useOptional}}{{/openApiNullable}}{{#useOptional}}{{#openApiNullable}}{{#isContainer}}{{^required}}{{>beanValidationCore}}{{/required}}{{/isContainer}}{{/openApiNullable}}{{#openApiNullable}}{{#required}}{{>beanValidationCore}}{{/required}}{{/openApiNullable}}{{/useOptional}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/bodyParams.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/bodyParams.mustache index 411135a6f963..23ff5ffc0474 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/bodyParams.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/bodyParams.mustache @@ -1 +1 @@ -{{#isBodyParam}}{{>paramDoc}}{{#useBeanValidation}} {{>beanValidationBodyParams}}{{#reactive}}@Valid {{/reactive}}{{^reactive}}{{^isContainer}}@Valid {{/isContainer}}{{/reactive}}{{/useBeanValidation}}{{^useBeanValidation}} {{/useBeanValidation}}@RequestBody{{^required}}(required = false){{/required}} {{^reactive}}{{>nullableAnnotation}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}} {{paramName}}{{/isBodyParam}} \ No newline at end of file +{{#isBodyParam}}{{>paramDoc}}{{#useBeanValidation}} {{>beanValidationBodyParams}}{{#reactive}}@Valid {{/reactive}}{{^reactive}}{{^isContainer}}{{^useOptional}}@Valid {{/useOptional}}{{#useOptional}}{{#required}}@Valid {{/required}}{{/useOptional}}{{/isContainer}}{{/reactive}}{{/useBeanValidation}}{{^useBeanValidation}} {{/useBeanValidation}}@RequestBody{{^required}}(required = false){{/required}} {{^reactive}}{{>nullableAnnotation}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}} {{paramName}}{{/isBodyParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/formParams.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/formParams.mustache index ca41fa6a80a9..40661448db5c 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/formParams.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/formParams.mustache @@ -1 +1 @@ -{{#isFormParam}}{{^isFile}}{{>paramDoc}}{{#useBeanValidation}} {{>beanValidationBodyParams}}{{^isContainer}}{{#isModel}}@Valid {{/isModel}}{{^isModel}}{{^useOptional}}@Valid {{/useOptional}}{{#useOptional}}{{#required}}@Valid {{/required}}{{/useOptional}}{{/isModel}}{{/isContainer}}{{/useBeanValidation}}{{^useBeanValidation}} {{/useBeanValidation}}{{#isModel}}@RequestPart{{/isModel}}{{^isModel}}{{#isArray}}@RequestPart{{/isArray}}{{^isArray}}{{#reactive}}@RequestPart{{/reactive}}{{^reactive}}@RequestParam{{/reactive}}{{/isArray}}{{/isModel}}(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}){{>dateTimeParam}} {{^required}}{{#useOptional}}Optional<{{/useOptional}}{{/required}}{{{dataType}}}{{^required}}{{#useOptional}}>{{/useOptional}}{{/required}} {{paramName}}{{/isFile}}{{#isFile}}{{>paramDoc}} {{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}@RequestPart(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{#reactive}}{{#isArray}}Flux<{{/isArray}}Part{{#isArray}}>{{/isArray}}{{/reactive}}{{^reactive}}{{#isArray}}List<{{/isArray}}MultipartFile{{#isArray}}>{{/isArray}}{{/reactive}} {{paramName}}{{/isFile}}{{/isFormParam}} \ No newline at end of file +{{#isFormParam}}{{^isFile}}{{>paramDoc}}{{#useBeanValidation}} {{>beanValidationBodyParams}}{{^isContainer}}{{^useOptional}}@Valid {{/useOptional}}{{#useOptional}}{{#required}}@Valid {{/required}}{{/useOptional}}{{/isContainer}}{{/useBeanValidation}}{{^useBeanValidation}} {{/useBeanValidation}}{{#isModel}}@RequestPart{{/isModel}}{{^isModel}}{{#isArray}}@RequestPart{{/isArray}}{{^isArray}}{{#reactive}}@RequestPart{{/reactive}}{{^reactive}}@RequestParam{{/reactive}}{{/isArray}}{{/isModel}}(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}){{>dateTimeParam}} {{#lambda.typeUseAnnotations}}{{^required}}{{#useOptional}}Optional<{{#useBeanValidation}}{{^isContainer}}{{#isModel}}@Valid {{/isModel}}{{/isContainer}}{{/useBeanValidation}}{{/useOptional}}{{/required}}{{{dataType}}}{{/lambda.typeUseAnnotations}}{{^required}}{{#useOptional}}>{{/useOptional}}{{/required}} {{paramName}}{{/isFile}}{{#isFile}}{{>paramDoc}} {{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}@RequestPart(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{#reactive}}{{#isArray}}Flux<{{/isArray}}Part{{#isArray}}>{{/isArray}}{{/reactive}}{{^reactive}}{{#isArray}}List<{{/isArray}}MultipartFile{{#isArray}}>{{/isArray}}{{/reactive}} {{paramName}}{{/isFile}}{{/isFormParam}} \ 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..73bd271c47a3 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}}{{#lambda.typeUseAnnotations}}{{#openApiNullable}}{{#isNullable}}{{^isContainer}}JsonNullable<{{#useBeanValidation}}{{#isModel}}@Valid {{/isModel}}{{>beanValidationCore}}{{/useBeanValidation}}{{/isContainer}}{{#isContainer}}JsonNullable<{{/isContainer}}{{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}Optional<{{#useBeanValidation}}{{#isModel}}@Valid {{/isModel}}{{>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.typeUseAnnotations}}{{/lambda.trim}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/optionalDataType.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/optionalDataType.mustache index feb2ecf37e1d..431f94ed32e6 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/optionalDataType.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/optionalDataType.mustache @@ -1 +1 @@ -{{#useOptional}}{{#required}}{{{dataType}}}{{/required}}{{^required}}Optional<{{#useBeanValidation}}{{>beanValidationCore}}{{/useBeanValidation}}{{{dataType}}}>{{/required}}{{/useOptional}}{{^useOptional}}{{#lambda.jSpecifyDatatype}}{{{dataType}}}{{/lambda.jSpecifyDatatype}}{{/useOptional}} \ No newline at end of file +{{#useOptional}}{{#required}}{{{dataType}}}{{/required}}{{^required}}{{#lambda.typeUseAnnotations}}Optional<{{#useBeanValidation}}{{^isContainer}}{{#isModel}}@Valid {{/isModel}}{{/isContainer}}{{>beanValidationCore}}{{/useBeanValidation}}{{{dataType}}}{{/lambda.typeUseAnnotations}}>{{/required}}{{/useOptional}}{{^useOptional}}{{#lambda.jSpecifyDatatype}}{{{dataType}}}{{/lambda.jSpecifyDatatype}}{{/useOptional}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/queryParams.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/queryParams.mustache index 24382e72ac11..b7c18e5d3f00 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/queryParams.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/queryParams.mustache @@ -1 +1 @@ -{{#isQueryParam}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{>paramDoc}}{{#useBeanValidation}}{{^isContainer}}{{#isModel}} @Valid{{/isModel}}{{^isModel}}{{^useOptional}} @Valid{{/useOptional}}{{#useOptional}}{{#required}} @Valid{{/required}}{{/useOptional}}{{/isModel}}{{/isContainer}}{{/useBeanValidation}}{{^isModel}} @RequestParam(value = {{#isMap}}""{{/isMap}}{{^isMap}}"{{baseName}}"{{/isMap}}{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/isModel}}{{>dateTimeParam}}{{#isDeprecated}} @Deprecated{{/isDeprecated}} {{>nullableAnnotation}}{{>optionalDataType}} {{paramName}}{{/isQueryParam}} \ No newline at end of file +{{#isQueryParam}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{>paramDoc}}{{#useBeanValidation}}{{^isContainer}}{{^useOptional}} @Valid{{/useOptional}}{{#useOptional}}{{#required}} @Valid{{/required}}{{/useOptional}}{{/isContainer}}{{/useBeanValidation}}{{^isModel}} @RequestParam(value = {{#isMap}}""{{/isMap}}{{^isMap}}"{{baseName}}"{{/isMap}}{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/isModel}}{{>dateTimeParam}}{{#isDeprecated}} @Deprecated{{/isDeprecated}} {{>nullableAnnotation}}{{>optionalDataType}} {{paramName}}{{/isQueryParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/AbstractJavaCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/AbstractJavaCodegenTest.java index 07f6e461de48..014901ba05f3 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/AbstractJavaCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/AbstractJavaCodegenTest.java @@ -1251,4 +1251,32 @@ public void testNullTypeMapsToObject() { assertThat(codegen.getTypeDeclaration(nullSchema)).isEqualTo("Object"); assertThat(codegen.getTypeDeclaration(new ArraySchema().items(nullSchema))).isEqualTo("List"); } + + @Test + public void placeTypeUseAnnotations_issue25097() { + // simple names and declarations without annotations are left untouched + assertThat(AbstractJavaCodegen.placeTypeUseAnnotations("Optional<@Valid Pet")).isEqualTo("Optional<@Valid Pet"); + assertThat(AbstractJavaCodegen.placeTypeUseAnnotations("@Valid Pet")).isEqualTo("@Valid Pet"); + assertThat(AbstractJavaCodegen.placeTypeUseAnnotations("Optional")).isEqualTo("Optional"); + assertThat(AbstractJavaCodegen.placeTypeUseAnnotations("java.time.Instant")).isEqualTo("java.time.Instant"); + // fully qualified type arguments get the annotations after the package qualifier + assertThat(AbstractJavaCodegen.placeTypeUseAnnotations("Optional<@Valid java.time.Instant")) + .isEqualTo("Optional")) + .isEqualTo("com.acme.@Valid Container"); + // dots and parentheses inside annotation names and string attributes are not mistaken for the qualifier + assertThat(AbstractJavaCodegen.placeTypeUseAnnotations( + "Optional<@Pattern(regexp = \"^(a.b\\\"c)$\") @jakarta.validation.constraints.Email java.lang.String")) + .isEqualTo("Optional>")) + .isEqualTo("JsonNullable>"); + } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java index 33e78b1e4847..42b6c0d30956 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java @@ -2647,6 +2647,48 @@ public void testBeanValidationOnContainerTypeArgument_issue23614() { TestUtils.assertFileNotContains(mixed, "@Valid Map"); } + @DataProvider(name = "beanValidationLibraries_issue25097") + public static Object[][] beanValidationLibraries_issue25097() { + return new Object[][]{{JavaClientCodegen.RESTTEMPLATE}, {JavaClientCodegen.NATIVE}, {JavaClientCodegen.WEBCLIENT}}; + } + + @Test(dataProvider = "beanValidationLibraries_issue25097") + public void testBeanValidationOnContainerModelGetters_issue25097(String library) { + final Path output = newTempFolder(); + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName(JAVA_GENERATOR) + .setLibrary(library) + .addAdditionalProperty(JavaClientCodegen.USE_BEANVALIDATION, true) + .addAdditionalProperty(JavaClientCodegen.OPENAPI_NULLABLE, true) + .setInputSpec("src/test/resources/3_0/spring/issue_25097_valid_container.yaml") + .setOutputDir(output.toString().replace("\\", "/")); + + List files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate(); + validateJavaSourceFiles(files); + + // DTO getters of container properties carry @Valid only on the type argument, never on the getter + // itself (HV000271), for lists, sets, maps, nested containers and allOf/oneOf/anyOf elements. + final JavaFileAssert zoo = JavaFileAssert.assertThat(output.resolve("src/main/java/org/openapitools/client/model/Zoo.java")); + final Map containerGetters = new LinkedHashMap<>(); + containerGetters.put("getExactSources", "List<@Valid Item>"); + containerGetters.put("getCats", "List<@Valid Cat>"); + containerGetters.put("getBreeders", "List<@Valid Breeder>"); + containerGetters.put("getAnyPets", "Set<@Valid AnyPet>"); + containerGetters.put("getRelatedItems", "Map"); + containerGetters.put("getCatsByName", "Map"); + containerGetters.put("getNestedItemMaps", "Map>"); + containerGetters.put("getListOfItemMaps", "List>"); + containerGetters.put("getMapOfItemLists", "Map>"); + containerGetters.put("getFreeForms", "List>"); + containerGetters.put("getColors", "List"); + containerGetters.forEach((getter, type) -> zoo.assertMethod(getter).hasReturnType(type) + .assertMethodAnnotations().doesNotContainWithName("Valid")); + zoo.assertMethod("getExactSources").assertMethodAnnotations().containsWithName("NotNull"); + // single (unwrapped) objects keep the member-level @Valid + zoo.assertMethod("getSingleItem").hasReturnType("Item").assertMethodAnnotations().containsWithName("Valid"); + zoo.assertMethod("getNullableSingleItem").hasReturnType("Item").assertMethodAnnotations().containsWithName("Valid"); + } + @Test public void testRestTemplateWithPerformBeanValidationEnabled() { final Path output = newTempFolder(); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/ParameterAssert.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/ParameterAssert.java index c9296c63e463..6033ad68a775 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/ParameterAssert.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/ParameterAssert.java @@ -44,6 +44,19 @@ public ParameterAssert hasType(final String expectedType) { return this; } + /** + * Like {@link #hasType(String)}, but keeps type-use annotations, e.g. {@code List<@Valid Pet>}. + * + * @param expectedType expected type including its type-use annotations + * @return this assert + */ + public ParameterAssert hasAnnotatedType(final String expectedType) { + Assertions.assertThat(actual.getType().toString()) + .withFailMessage("Expected parameter to have type %s, but was %s", expectedType, actual.getType().toString()) + .isEqualTo(expectedType); + return this; + } + public ParameterAnnotationsAssert assertParameterAnnotations() { return new ParameterAnnotationsAssert(this, actual.getAnnotations()); } 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 ed8c0964c47e..cf0946395bc4 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 @@ -1138,6 +1138,210 @@ public void beanValidationOnContainerParameter_issue23614() throws IOException { .fileDoesNotContain("@Valid @RequestParam(value = \"status\", required = true) List"); } + private static final String ISSUE_25097_SPEC = "src/test/resources/3_0/spring/issue_25097_valid_container.yaml"; + + @Test + public void beanValidationOnListParameters_issue25097() throws IOException { + final SpringCodegen codegen = new SpringCodegen(); + codegen.setUseBeanValidation(true); + + final Map files = generateFiles(codegen, ISSUE_25097_SPEC); + + // Variant 1: container constraints (@NotNull/@Size) stay on the container, element constraints and + // cascaded validation go on the type argument, and the container-level @Valid (HV000271) is gone. + JavaFileAssert.assertThat(files.get("ItemsApi.java")) + .assertMethod("findItems") + .assertParameter("itemCodes").hasAnnotatedType("List<@Size(min = 1, max = 10) String>") + .assertParameterAnnotations() + .containsWithName("NotNull") + .containsWithNameAndAttributes("Size", ImmutableMap.of("max", "8")) + .doesNotContainWithName("Valid") + .toParameter().toMethod() + .assertParameter("filter").hasAnnotatedType("Map") + .assertParameterAnnotations() + .doesNotContainWithName("Valid") + .toParameter().toMethod().toFileAssert() + .assertMethod("updateItems") + .assertParameter("item").hasAnnotatedType("List<@Valid Item>") + .assertParameterAnnotations() + .containsWithNameAndAttributes("Size", ImmutableMap.of("max", "500")) + .doesNotContainWithName("Valid"); + + // A map request body cascades through the value type argument. + JavaFileAssert.assertThat(files.get("ItemMapApi.java")) + .assertMethod("saveItemMap") + .assertParameter("requestBody").hasAnnotatedType("Map") + .assertParameterAnnotations() + .doesNotContainWithName("Valid"); + + // Single (non-container) bodies keep the parameter-level @Valid. + JavaFileAssert.assertThat(files.get("RequiredBodyApi.java")) + .assertMethod("saveRequiredItem") + .assertParameter("item").hasType("Item") + .assertParameterAnnotations() + .containsWithName("Valid"); + } + + @Test + public void beanValidationOnComposedArrayItems_issue25097() throws IOException { + final SpringCodegen codegen = new SpringCodegen(); + codegen.setUseBeanValidation(true); + + final Map files = generateFiles(codegen, ISSUE_25097_SPEC); + + // Variant 3b: once the container-level @Valid is gone, arrays/sets of allOf/oneOf/anyOf models must + // carry @Valid on the type argument, otherwise element validation is silently lost. + JavaFileAssert.assertThat(files.get("KittensApi.java")) + .assertMethod("saveKittens") + .assertParameter("cat").hasAnnotatedType("List<@Valid Cat>") + .assertParameterAnnotations().doesNotContainWithName("Valid"); + JavaFileAssert.assertThat(files.get("BreedersApi.java")) + .assertMethod("saveBreeders") + .assertParameter("breeder").hasAnnotatedType("Set<@Valid Breeder>") + .assertParameterAnnotations().doesNotContainWithName("Valid"); + JavaFileAssert.assertThat(files.get("AnyPetsApi.java")) + .assertMethod("saveAnyPets") + .assertParameter("anyPet").hasAnnotatedType("List<@Valid AnyPet>") + .assertParameterAnnotations().doesNotContainWithName("Valid"); + + JavaFileAssert.assertThat(files.get("Zoo.java")) + .fileContains( + "private List<@Valid Cat> cats", // allOf + "private List<@Valid Breeder> breeders", // oneOf + "private Set<@Valid AnyPet> anyPets", // anyOf + "public List<@Valid Cat> getCats()", + "public List<@Valid Breeder> getBreeders()", + "public Set<@Valid AnyPet> getAnyPets()", + // free-form objects and enums are not cascadable + "public List> getFreeForms()", + "public List getColors()") + .fileDoesNotContain("@Valid Color", "@Valid Map", "@Valid Object"); + } + + @Test + public void beanValidationOnContainerModelGetters_issue25097() throws IOException { + final SpringCodegen codegen = new SpringCodegen(); + codegen.setUseBeanValidation(true); + + final Map files = generateFiles(codegen, ISSUE_25097_SPEC); + + // Variant 2: container getters only carry @Valid on the type argument, never on the getter itself. + final JavaFileAssert zoo = JavaFileAssert.assertThat(files.get("Zoo.java")); + zoo.assertMethod("getExactSources").hasReturnType("List<@Valid Item>") + .assertMethodAnnotations().containsWithName("NotNull").doesNotContainWithName("Valid"); + // Variant 3a: map values (also nested and nullable) cascade through the value type argument. + zoo.assertMethod("getRelatedItems").hasReturnType("Map") + .assertMethodAnnotations().doesNotContainWithName("Valid"); + zoo.assertMethod("getCatsByName").hasReturnType("Map") + .assertMethodAnnotations().doesNotContainWithName("Valid"); + zoo.assertMethod("getNestedItemMaps").hasReturnType("Map>") + .assertMethodAnnotations().doesNotContainWithName("Valid"); + zoo.assertMethod("getListOfItemMaps").hasReturnType("List>") + .assertMethodAnnotations().doesNotContainWithName("Valid"); + zoo.assertMethod("getMapOfItemLists").hasReturnType("Map>") + .assertMethodAnnotations().doesNotContainWithName("Valid"); + zoo.assertMethod("getNullableItemMap").hasReturnType("JsonNullable>") + .assertMethodAnnotations().doesNotContainWithName("Valid"); + zoo.assertMethod("getCodes").hasReturnType("List<@Size(max = 3) String>") + .assertMethodAnnotations().doesNotContainWithName("Valid"); + // A plain (unwrapped) single object keeps the member-level @Valid. + zoo.assertMethod("getSingleItem").hasReturnType("Item") + .assertMethodAnnotations().containsWithName("Valid"); + } + + @Test + public void beanValidationOnJsonNullableModel_issue25097() throws IOException { + final SpringCodegen codegen = new SpringCodegen(); + codegen.setUseBeanValidation(true); + codegen.setOpenApiNullable(true); + + final Map files = generateFiles(codegen, ISSUE_25097_SPEC); + + // JsonNullable is a container for Hibernate Validator (jackson-databind-nullable registers a + // ValueExtractor), so a member-level @Valid on it logs HV000271: cascade through the type argument. + JavaFileAssert.assertThat(files.get("Zoo.java")) + .fileContains("private JsonNullable<@Valid Item> nullableSingleItem = JsonNullable.undefined();", + "public JsonNullable getNullableAttachment()") + .assertMethod("getNullableSingleItem").hasReturnType("JsonNullable<@Valid Item>") + .assertMethodAnnotations().doesNotContainWithName("Valid"); + } + + @Test + public void beanValidationOnOptionalModel_issue25097() throws IOException { + final SpringCodegen codegen = new SpringCodegen(); + codegen.setUseBeanValidation(true); + codegen.setUseOptional(true); + codegen.setOpenApiNullable(true); + + final Map files = generateFiles(codegen, ISSUE_25097_SPEC); + + // Optional is a container for Hibernate Validator: @Valid goes on its type argument. + JavaFileAssert.assertThat(files.get("Zoo.java")) + .fileContains("private Optional<@Valid Item> singleItem = Optional.empty();") + .assertMethod("getSingleItem").hasReturnType("Optional<@Valid Item>") + .assertMethodAnnotations().doesNotContainWithName("Valid") + .toMethod().toFileAssert() + .assertMethod("getNullableSingleItem").hasReturnType("JsonNullable<@Valid Item>") + .assertMethodAnnotations().doesNotContainWithName("Valid") + .toMethod().toFileAssert() + // non-model values (here a fully qualified binary type) have nothing to cascade into: no @Valid at all + .fileContains( + "private Optional attachment = Optional.empty();", + "public Optional getAttachment()", + "public JsonNullable getNullableAttachment()") + .assertMethod("getAttachment").assertMethodAnnotations().doesNotContainWithName("Valid") + .toMethod().toFileAssert() + .assertMethod("getNullableAttachment").assertMethodAnnotations().doesNotContainWithName("Valid"); + + // Optional model query parameter (deepObject); the required one is not wrapped and keeps @Valid. + JavaFileAssert.assertThat(files.get("SearchApi.java")) + .assertMethod("searchItems") + .assertParameter("criteria").hasAnnotatedType("Optional<@Valid Item>") + .assertParameterAnnotations().doesNotContainWithName("Valid") + .toParameter().toMethod() + .assertParameter("requiredCriteria").hasType("Animal") + .assertParameterAnnotations().containsWithName("Valid"); + + // Optional model multipart part, and an optional list part keeps the element @Valid. + JavaFileAssert.assertThat(files.get("UploadApi.java")) + .assertMethod("uploadItem") + .assertParameter("item").hasAnnotatedType("Optional<@Valid Item>") + .assertParameterAnnotations().doesNotContainWithName("Valid") + .toParameter().toMethod() + .assertParameter("items").hasAnnotatedType("Optional>") + .assertParameterAnnotations().doesNotContainWithName("Valid"); + + // Optional (not required) request body vs. required request body. + JavaFileAssert.assertThat(files.get("OptionalBodyApi.java")) + .assertMethod("saveOptionalItem") + .assertParameter("item").hasAnnotatedType("Optional<@Valid Item>") + .assertParameterAnnotations().doesNotContainWithName("Valid"); + JavaFileAssert.assertThat(files.get("RequiredBodyApi.java")) + .assertMethod("saveRequiredItem") + .assertParameter("item").hasType("Item") + .assertParameterAnnotations().containsWithName("Valid"); + } + + @Test + public void beanValidationOnOptionalModelLombok_issue25097() throws IOException { + final SpringCodegen codegen = new SpringCodegen(); + codegen.setUseBeanValidation(true); + codegen.setUseOptional(true); + codegen.setOpenApiNullable(true); + codegen.additionalProperties().put(AbstractJavaCodegen.ADDITIONAL_MODEL_TYPE_ANNOTATIONS, "@lombok.Data"); + + final Map files = generateFiles(codegen, ISSUE_25097_SPEC); + + // With Lombok-generated accessors, bean validation is declared on the field. + JavaFileAssert.assertThat(files.get("Zoo.java")) + .fileContains( + "private Optional<@Valid Item> singleItem = Optional.empty();", + "private JsonNullable<@Valid Item> nullableSingleItem = JsonNullable.undefined();") + .assertProperty("singleItem").assertPropertyAnnotations().doesNotContainWithName("Valid") + .toProperty().toType() + .assertProperty("nullableSingleItem").assertPropertyAnnotations().doesNotContainWithName("Valid"); + } + @Test public void testXImplements() throws IOException { final SpringCodegen codegen = new SpringCodegen(); @@ -4831,7 +5035,8 @@ public void testModelsWithOptionalAndJsonNullable() throws IOException { .toType() .fileContains("stringDefault = Optional.of(\"ABC\")") .assertProperty("zebra") - .withType("Optional") + // a wrapped model cascades through the type argument (HV000271 on a member-level @Valid Optional) + .withType("Optional<@Valid Zebra>") .toType() .assertProperty("stringPatternNullable") @@ -4944,7 +5149,7 @@ public void testModelsWithOptionalAndJsonNullable() throws IOException { assertOptionalMethod(javaFileAssert, BigDecimal.class, "numberMinMax", "Optional<@DecimalMin(value = \"1\") @DecimalMax(value = \"10\") BigDecimal>"); assertOptionalMethod(javaFileAssert, BigDecimal.class, "numberMin", "Optional<@DecimalMin(value = \"1\") BigDecimal>"); assertOptionalMethod(javaFileAssert, BigDecimal.class, "numberMax", "Optional<@DecimalMax(value = \"10\") BigDecimal>"); - assertOptionalMethod(javaFileAssert, "Zebra", "zebra", "Optional"); + assertOptionalMethod(javaFileAssert, "Zebra", "zebra", "Optional<@Valid Zebra>"); assertJsonNullableMethod(javaFileAssert, String.class, "stringPatternNullable", "JsonNullable<@Pattern(regexp = \"[a-z]\") String>"); assertJsonNullableMethod(javaFileAssert, String.class, "stringMaxMinLengthNullable", "JsonNullable<@Size(min = 1, max = 10) String>"); @@ -9082,7 +9287,8 @@ void schemaMappingWithNullableAllOfRendersNullableJavaProperty() throws IOExcept configurator -> configurator.addSchemaMapping("ExternalModel", "com.example.ExternalModel")); JavaFileAssert.assertThat(files.get("MyObject.java")) - .assertProperty("optionalRef").withType("JsonNullable"); + // a fully qualified type argument carries its type-use @Valid after the package qualifier + .assertProperty("optionalRef").withType("JsonNullable"); } @Test diff --git a/modules/openapi-generator/src/test/resources/3_0/spring/issue_25097_valid_container.yaml b/modules/openapi-generator/src/test/resources/3_0/spring/issue_25097_valid_container.yaml new file mode 100644 index 000000000000..744da1a4a1f4 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/spring/issue_25097_valid_container.yaml @@ -0,0 +1,292 @@ +openapi: 3.0.3 +info: + title: Bean validation @Valid placement on containers and wrappers (issue 25097) + version: 1.0.0 +paths: + /items: + get: + operationId: findItems + parameters: + - name: itemCodes + in: query + required: true + schema: + type: array + maxItems: 8 + items: + type: string + minLength: 1 + maxLength: 10 + - name: filter + in: query + required: false + style: form + explode: true + schema: + type: object + additionalProperties: + type: string + responses: + '200': + description: ok + post: + operationId: updateItems + requestBody: + required: true + content: + application/json: + schema: + type: array + maxItems: 500 + items: + $ref: '#/components/schemas/Item' + responses: + '200': + description: ok + /search: + get: + operationId: searchItems + parameters: + - name: criteria + in: query + required: false + style: deepObject + schema: + $ref: '#/components/schemas/Item' + - name: requiredCriteria + in: query + required: true + style: deepObject + schema: + $ref: '#/components/schemas/Animal' + responses: + '200': + description: ok + /upload: + post: + operationId: uploadItem + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + item: + $ref: '#/components/schemas/Item' + items: + type: array + items: + $ref: '#/components/schemas/Item' + responses: + '200': + description: ok + /optionalBody: + post: + operationId: saveOptionalItem + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + responses: + '200': + description: ok + /requiredBody: + post: + operationId: saveRequiredItem + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + responses: + '200': + description: ok + /kittens: + post: + operationId: saveKittens + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Cat' + responses: + '200': + description: ok + /breeders: + post: + operationId: saveBreeders + requestBody: + required: true + content: + application/json: + schema: + type: array + uniqueItems: true + items: + $ref: '#/components/schemas/Breeder' + responses: + '200': + description: ok + /itemMap: + post: + operationId: saveItemMap + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: + $ref: '#/components/schemas/Item' + responses: + '200': + description: ok + /anyPets: + post: + operationId: saveAnyPets + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/AnyPet' + responses: + '200': + description: ok +components: + schemas: + Item: + type: object + required: [code] + properties: + code: + type: string + maxLength: 10 + Animal: + type: object + required: [name] + properties: + name: + type: string + Cat: + allOf: + - $ref: '#/components/schemas/Animal' + - type: object + properties: + lives: + type: integer + Dog: + allOf: + - $ref: '#/components/schemas/Animal' + - type: object + properties: + barks: + type: boolean + Breeder: + oneOf: + - $ref: '#/components/schemas/Animal' + - $ref: '#/components/schemas/Dog' + AnyPet: + anyOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Dog' + FreeForm: + type: object + additionalProperties: true + Color: + type: string + enum: [red, blue] + Zoo: + type: object + required: [exactSources] + properties: + exactSources: + type: array + items: + $ref: '#/components/schemas/Item' + animals: + type: array + items: + $ref: '#/components/schemas/Animal' + cats: + type: array + items: + $ref: '#/components/schemas/Cat' + breeders: + type: array + items: + $ref: '#/components/schemas/Breeder' + anyPets: + type: array + uniqueItems: true + items: + $ref: '#/components/schemas/AnyPet' + relatedItems: + type: object + additionalProperties: + $ref: '#/components/schemas/Item' + catsByName: + type: object + additionalProperties: + $ref: '#/components/schemas/Cat' + nestedItemMaps: + type: object + additionalProperties: + type: object + additionalProperties: + $ref: '#/components/schemas/Item' + listOfItemMaps: + type: array + items: + type: object + additionalProperties: + $ref: '#/components/schemas/Item' + mapOfItemLists: + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/Item' + nullableItemMap: + type: object + nullable: true + additionalProperties: + $ref: '#/components/schemas/Item' + freeForms: + type: array + items: + $ref: '#/components/schemas/FreeForm' + freeFormMap: + type: object + additionalProperties: + $ref: '#/components/schemas/FreeForm' + colors: + type: array + items: + $ref: '#/components/schemas/Color' + codes: + type: array + items: + type: string + maxLength: 3 + singleItem: + $ref: '#/components/schemas/Item' + nullableSingleItem: + nullable: true + allOf: + - $ref: '#/components/schemas/Item' + attachment: + type: string + format: binary + nullableAttachment: + type: string + format: binary + nullable: true diff --git a/samples/openapi3/client/petstore/spring-cloud-3-with-optional/src/main/java/org/openapitools/model/Order.java b/samples/openapi3/client/petstore/spring-cloud-3-with-optional/src/main/java/org/openapitools/model/Order.java index c88443af52bb..1450775c551c 100644 --- a/samples/openapi3/client/petstore/spring-cloud-3-with-optional/src/main/java/org/openapitools/model/Order.java +++ b/samples/openapi3/client/petstore/spring-cloud-3-with-optional/src/main/java/org/openapitools/model/Order.java @@ -150,7 +150,7 @@ public Order shipDate(OffsetDateTime shipDate) { * Get shipDate * @return shipDate */ - @Valid + @JsonProperty("shipDate") public Optional getShipDate() { return shipDate; diff --git a/samples/openapi3/client/petstore/spring-cloud-3-with-optional/src/main/java/org/openapitools/model/Pet.java b/samples/openapi3/client/petstore/spring-cloud-3-with-optional/src/main/java/org/openapitools/model/Pet.java index 1b944413a7ae..668759b3a66f 100644 --- a/samples/openapi3/client/petstore/spring-cloud-3-with-optional/src/main/java/org/openapitools/model/Pet.java +++ b/samples/openapi3/client/petstore/spring-cloud-3-with-optional/src/main/java/org/openapitools/model/Pet.java @@ -32,7 +32,7 @@ public class Pet { private Optional id = Optional.empty(); @JsonInclude(JsonInclude.Include.NON_NULL) - private Optional category = Optional.empty(); + private Optional<@Valid Category> category = Optional.empty(); @JsonInclude(JsonInclude.Include.NON_NULL) private String name; @@ -127,9 +127,9 @@ public Pet category(Category category) { * Get category * @return category */ - @Valid + @JsonProperty("category") - public Optional getCategory() { + public Optional<@Valid Category> getCategory() { return category; } diff --git a/samples/openapi3/server/petstore/springboot-4-jspecify-openapiNullable/src/main/java/org/openapitools/model/Foo.java b/samples/openapi3/server/petstore/springboot-4-jspecify-openapiNullable/src/main/java/org/openapitools/model/Foo.java index 17209df941e8..1a17f9ecac51 100644 --- a/samples/openapi3/server/petstore/springboot-4-jspecify-openapiNullable/src/main/java/org/openapitools/model/Foo.java +++ b/samples/openapi3/server/petstore/springboot-4-jspecify-openapiNullable/src/main/java/org/openapitools/model/Foo.java @@ -130,7 +130,7 @@ public Foo nullableDt(java.time.@Nullable Instant nullableDt) { * Get nullableDt * @return nullableDt */ - @Valid + @Schema(name = "nullableDt", requiredMode = Schema.RequiredMode.NOT_REQUIRED, nullable = true) @JsonProperty("nullableDt") @JacksonXmlProperty(localName = "nullableDt") @@ -176,7 +176,7 @@ public Foo nullableBinary(org.springframework.core.io.@Nullable Resource nullabl * Get nullableBinary * @return nullableBinary */ - @Valid + @Schema(name = "nullableBinary", requiredMode = Schema.RequiredMode.NOT_REQUIRED, nullable = true) @JsonProperty("nullableBinary") @JacksonXmlProperty(localName = "nullableBinary") @@ -345,7 +345,7 @@ public Foo nullableNumber(java.math.@Nullable BigDecimal nullableNumber) { * Get nullableNumber * @return nullableNumber */ - @Valid + @Schema(name = "nullableNumber", requiredMode = Schema.RequiredMode.NOT_REQUIRED, nullable = true) @JsonProperty("nullableNumber") @JacksonXmlProperty(localName = "nullableNumber") diff --git a/samples/openapi3/server/petstore/springboot-4-jspecify-openapiNullable/src/main/java/org/openapitools/model/RequiredAndNullable.java b/samples/openapi3/server/petstore/springboot-4-jspecify-openapiNullable/src/main/java/org/openapitools/model/RequiredAndNullable.java index 3288565a5234..6da6d1866704 100644 --- a/samples/openapi3/server/petstore/springboot-4-jspecify-openapiNullable/src/main/java/org/openapitools/model/RequiredAndNullable.java +++ b/samples/openapi3/server/petstore/springboot-4-jspecify-openapiNullable/src/main/java/org/openapitools/model/RequiredAndNullable.java @@ -90,7 +90,7 @@ public RequiredAndNullable file(org.springframework.core.io.@Nullable Resource f * Get file * @return file */ - /* @Present */ @Valid + /* @Present */ @Schema(name = "file", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true) @JsonProperty("file") @JacksonXmlProperty(localName = "file") diff --git a/samples/openapi3/server/petstore/springboot-4-jspecify-useOptional/src/main/java/org/openapitools/model/Foo.java b/samples/openapi3/server/petstore/springboot-4-jspecify-useOptional/src/main/java/org/openapitools/model/Foo.java index 883bd19c6fa7..f75c0fba23a3 100644 --- a/samples/openapi3/server/petstore/springboot-4-jspecify-useOptional/src/main/java/org/openapitools/model/Foo.java +++ b/samples/openapi3/server/petstore/springboot-4-jspecify-useOptional/src/main/java/org/openapitools/model/Foo.java @@ -106,7 +106,7 @@ public Foo dt(java.time.@Nullable Instant dt) { * Get dt * @return dt */ - @Valid + @Schema(name = "dt", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @JsonProperty("dt") @JacksonXmlProperty(localName = "dt") @@ -130,7 +130,7 @@ public Foo nullableDt(java.time.@Nullable Instant nullableDt) { * Get nullableDt * @return nullableDt */ - @Valid + @Schema(name = "nullableDt", requiredMode = Schema.RequiredMode.NOT_REQUIRED, nullable = true) @JsonProperty("nullableDt") @JacksonXmlProperty(localName = "nullableDt") @@ -152,7 +152,7 @@ public Foo binary(org.springframework.core.io.@Nullable Resource binary) { * Get binary * @return binary */ - @Valid + @Schema(name = "binary", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @JsonProperty("binary") @JacksonXmlProperty(localName = "binary") @@ -176,7 +176,7 @@ public Foo nullableBinary(org.springframework.core.io.@Nullable Resource nullabl * Get nullableBinary * @return nullableBinary */ - @Valid + @Schema(name = "nullableBinary", requiredMode = Schema.RequiredMode.NOT_REQUIRED, nullable = true) @JsonProperty("nullableBinary") @JacksonXmlProperty(localName = "nullableBinary") @@ -321,7 +321,7 @@ public Foo number(java.math.@Nullable BigDecimal number) { * Get number * @return number */ - @Valid + @Schema(name = "number", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @JsonProperty("number") @JacksonXmlProperty(localName = "number") @@ -345,7 +345,7 @@ public Foo nullableNumber(java.math.@Nullable BigDecimal nullableNumber) { * Get nullableNumber * @return nullableNumber */ - @Valid + @Schema(name = "nullableNumber", requiredMode = Schema.RequiredMode.NOT_REQUIRED, nullable = true) @JsonProperty("nullableNumber") @JacksonXmlProperty(localName = "nullableNumber") diff --git a/samples/openapi3/server/petstore/springboot-4-jspecify-useOptional/src/main/java/org/openapitools/model/RequiredAndNullable.java b/samples/openapi3/server/petstore/springboot-4-jspecify-useOptional/src/main/java/org/openapitools/model/RequiredAndNullable.java index 3288565a5234..6da6d1866704 100644 --- a/samples/openapi3/server/petstore/springboot-4-jspecify-useOptional/src/main/java/org/openapitools/model/RequiredAndNullable.java +++ b/samples/openapi3/server/petstore/springboot-4-jspecify-useOptional/src/main/java/org/openapitools/model/RequiredAndNullable.java @@ -90,7 +90,7 @@ public RequiredAndNullable file(org.springframework.core.io.@Nullable Resource f * Get file * @return file */ - /* @Present */ @Valid + /* @Present */ @Schema(name = "file", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true) @JsonProperty("file") @JacksonXmlProperty(localName = "file") diff --git a/samples/server/petstore/springboot-file-delegate-optional/src/main/java/org/openapitools/api/DummyApi.java b/samples/server/petstore/springboot-file-delegate-optional/src/main/java/org/openapitools/api/DummyApi.java index c42bc74caa29..2bb6cb6e1722 100644 --- a/samples/server/petstore/springboot-file-delegate-optional/src/main/java/org/openapitools/api/DummyApi.java +++ b/samples/server/petstore/springboot-file-delegate-optional/src/main/java/org/openapitools/api/DummyApi.java @@ -60,7 +60,7 @@ default DummyApiDelegate getDelegate() { consumes = { "application/octet-stream" } ) default ResponseEntity uploadFile( - @Parameter(name = "body", description = "") @Valid @RequestBody(required = false) Optional body + @Parameter(name = "body", description = "") @RequestBody(required = false) Optional body ) { return getDelegate().uploadFile(body); } diff --git a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/api/FakeApi.java b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/api/FakeApi.java index 281d87c7a317..4e621de147cd 100644 --- a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/api/FakeApi.java +++ b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/api/FakeApi.java @@ -110,7 +110,7 @@ default ResponseEntity createXmlItem( consumes = { "application/json" } ) default ResponseEntity fakeOuterBooleanSerialize( - @Parameter(name = "body", description = "Input boolean as post body") @Valid @RequestBody(required = false) Optional body + @Parameter(name = "body", description = "Input boolean as post body") @RequestBody(required = false) Optional body ) { return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); @@ -142,7 +142,7 @@ default ResponseEntity fakeOuterBooleanSerialize( consumes = { "application/json" } ) default ResponseEntity fakeOuterCompositeSerialize( - @Parameter(name = "OuterComposite", description = "Input composite as post body") @Valid @RequestBody(required = false) Optional outerComposite + @Parameter(name = "OuterComposite", description = "Input composite as post body") @RequestBody(required = false) Optional<@Valid OuterComposite> outerComposite ) { getRequest().ifPresent(request -> { for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { @@ -183,7 +183,7 @@ default ResponseEntity fakeOuterCompositeSerialize( consumes = { "application/json" } ) default ResponseEntity fakeOuterNumberSerialize( - @Parameter(name = "body", description = "Input number as post body") @Valid @RequestBody(required = false) Optional body + @Parameter(name = "body", description = "Input number as post body") @RequestBody(required = false) Optional body ) { return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); @@ -215,7 +215,7 @@ default ResponseEntity fakeOuterNumberSerialize( consumes = { "application/json" } ) default ResponseEntity fakeOuterStringSerialize( - @Parameter(name = "body", description = "Input string as post body") @Valid @RequestBody(required = false) Optional body + @Parameter(name = "body", description = "Input string as post body") @RequestBody(required = false) Optional body ) { return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); diff --git a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/EnumTest.java b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/EnumTest.java index 94f5b1f99760..a3b34b9bfcbf 100644 --- a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/EnumTest.java +++ b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/EnumTest.java @@ -290,7 +290,7 @@ public EnumTest outerEnum(OuterEnum outerEnum) { * Get outerEnum * @return outerEnum */ - @Valid + @Schema(name = "outerEnum", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @JsonProperty("outerEnum") public Optional getOuterEnum() { diff --git a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/FileSchemaTestClass.java b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/FileSchemaTestClass.java index 177ee4ce6905..3dc85c593bce 100644 --- a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/FileSchemaTestClass.java +++ b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/FileSchemaTestClass.java @@ -28,7 +28,7 @@ public class FileSchemaTestClass { @JsonInclude(JsonInclude.Include.NON_DEFAULT) - private Optional file = Optional.empty(); + private Optional<@Valid File> file = Optional.empty(); @JsonInclude(JsonInclude.Include.NON_DEFAULT) private List<@Valid File> files = new ArrayList<>(); @@ -42,10 +42,10 @@ public FileSchemaTestClass file(File file) { * Get file * @return file */ - @Valid + @Schema(name = "file", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @JsonProperty("file") - public Optional getFile() { + public Optional<@Valid File> getFile() { return file; } diff --git a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/FormatTest.java b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/FormatTest.java index f1dad67621d1..ab60726a7066 100644 --- a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/FormatTest.java +++ b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/FormatTest.java @@ -276,7 +276,7 @@ public FormatTest binary(org.springframework.core.io.Resource binary) { * Get binary * @return binary */ - @Valid + @Schema(name = "binary", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @JsonProperty("binary") public Optional getBinary() { @@ -318,7 +318,7 @@ public FormatTest dateTime(OffsetDateTime dateTime) { * Get dateTime * @return dateTime */ - @Valid + @Schema(name = "dateTime", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @JsonProperty("dateTime") public Optional getDateTime() { @@ -339,7 +339,7 @@ public FormatTest uuid(UUID uuid) { * Get uuid * @return uuid */ - @Valid + @Schema(name = "uuid", example = "72f98069-206d-4f12-9f12-3d1e525a8e84", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @JsonProperty("uuid") public Optional getUuid() { @@ -381,7 +381,7 @@ public FormatTest bigDecimal(BigDecimal bigDecimal) { * Get bigDecimal * @return bigDecimal */ - @Valid + @Schema(name = "BigDecimal", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @JsonProperty("BigDecimal") public Optional getBigDecimal() { diff --git a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/MixedPropertiesAndAdditionalPropertiesClass.java b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/MixedPropertiesAndAdditionalPropertiesClass.java index ec96152898fc..a325a202fad9 100644 --- a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/MixedPropertiesAndAdditionalPropertiesClass.java +++ b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/MixedPropertiesAndAdditionalPropertiesClass.java @@ -48,7 +48,7 @@ public MixedPropertiesAndAdditionalPropertiesClass uuid(UUID uuid) { * Get uuid * @return uuid */ - @Valid + @Schema(name = "uuid", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @JsonProperty("uuid") public Optional getUuid() { @@ -69,7 +69,7 @@ public MixedPropertiesAndAdditionalPropertiesClass dateTime(OffsetDateTime dateT * Get dateTime * @return dateTime */ - @Valid + @Schema(name = "dateTime", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @JsonProperty("dateTime") public Optional getDateTime() { diff --git a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/NumberOnly.java b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/NumberOnly.java index f4df88cebe27..2ab02917286b 100644 --- a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/NumberOnly.java +++ b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/NumberOnly.java @@ -36,7 +36,7 @@ public NumberOnly justNumber(BigDecimal justNumber) { * Get justNumber * @return justNumber */ - @Valid + @Schema(name = "JustNumber", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @JsonProperty("JustNumber") public Optional getJustNumber() { diff --git a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/Order.java b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/Order.java index 150ccecdd281..35b18e3f8cd7 100644 --- a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/Order.java +++ b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/Order.java @@ -154,7 +154,7 @@ public Order shipDate(OffsetDateTime shipDate) { * Get shipDate * @return shipDate */ - @Valid + @Schema(name = "shipDate", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @JsonProperty("shipDate") public Optional getShipDate() { diff --git a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/OuterComposite.java b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/OuterComposite.java index e0e3b46033ed..f118d4256bef 100644 --- a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/OuterComposite.java +++ b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/OuterComposite.java @@ -42,7 +42,7 @@ public OuterComposite myNumber(BigDecimal myNumber) { * Get myNumber * @return myNumber */ - @Valid + @Schema(name = "my_number", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @JsonProperty("my_number") public Optional getMyNumber() { diff --git a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/Pet.java b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/Pet.java index a6c302f1ce12..832e47ae6f8e 100644 --- a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/Pet.java +++ b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/Pet.java @@ -36,7 +36,7 @@ public class Pet { private Optional id = Optional.empty(); @JsonInclude(JsonInclude.Include.NON_DEFAULT) - private Optional category = Optional.empty(); + private Optional<@Valid Category> category = Optional.empty(); @JsonInclude(JsonInclude.Include.NON_NULL) private String name; @@ -132,10 +132,10 @@ public Pet category(Category category) { * Get category * @return category */ - @Valid + @Schema(name = "category", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @JsonProperty("category") - public Optional getCategory() { + public Optional<@Valid Category> getCategory() { return category; } diff --git a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/XmlItem.java b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/XmlItem.java index 40b0a0c96610..8916b3216aad 100644 --- a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/XmlItem.java +++ b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/model/XmlItem.java @@ -144,7 +144,7 @@ public XmlItem attributeNumber(BigDecimal attributeNumber) { * Get attributeNumber * @return attributeNumber */ - @Valid + @Schema(name = "attribute_number", example = "1.234", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @JsonProperty("attribute_number") public Optional getAttributeNumber() { @@ -257,7 +257,7 @@ public XmlItem nameNumber(BigDecimal nameNumber) { * Get nameNumber * @return nameNumber */ - @Valid + @Schema(name = "name_number", example = "1.234", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @JsonProperty("name_number") public Optional getNameNumber() { @@ -399,7 +399,7 @@ public XmlItem prefixNumber(BigDecimal prefixNumber) { * Get prefixNumber * @return prefixNumber */ - @Valid + @Schema(name = "prefix_number", example = "1.234", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @JsonProperty("prefix_number") public Optional getPrefixNumber() { @@ -541,7 +541,7 @@ public XmlItem namespaceNumber(BigDecimal namespaceNumber) { * Get namespaceNumber * @return namespaceNumber */ - @Valid + @Schema(name = "namespace_number", example = "1.234", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @JsonProperty("namespace_number") public Optional getNamespaceNumber() { @@ -683,7 +683,7 @@ public XmlItem prefixNsNumber(BigDecimal prefixNsNumber) { * Get prefixNsNumber * @return prefixNsNumber */ - @Valid + @Schema(name = "prefix_ns_number", example = "1.234", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @JsonProperty("prefix_ns_number") public Optional getPrefixNsNumber() { diff --git a/samples/server/petstore/springboot-x-implements-skip/src/main/java/org/openapitools/model/EnumTestDto.java b/samples/server/petstore/springboot-x-implements-skip/src/main/java/org/openapitools/model/EnumTestDto.java index 6a788fc57733..d474251edc62 100644 --- a/samples/server/petstore/springboot-x-implements-skip/src/main/java/org/openapitools/model/EnumTestDto.java +++ b/samples/server/petstore/springboot-x-implements-skip/src/main/java/org/openapitools/model/EnumTestDto.java @@ -304,7 +304,7 @@ public EnumTestDto outerEnum(OuterEnumDto outerEnum) { * Get outerEnum * @return outerEnum */ - @Valid + @Schema(name = "outerEnum", requiredMode = Schema.RequiredMode.NOT_REQUIRED, nullable = true) @JsonProperty("outerEnum") public JsonNullable getOuterEnum() { diff --git a/samples/server/petstore/springboot-x-implements-skip/src/main/java/org/openapitools/model/NullableClassDto.java b/samples/server/petstore/springboot-x-implements-skip/src/main/java/org/openapitools/model/NullableClassDto.java index 17c478906462..8bf21884cbd8 100644 --- a/samples/server/petstore/springboot-x-implements-skip/src/main/java/org/openapitools/model/NullableClassDto.java +++ b/samples/server/petstore/springboot-x-implements-skip/src/main/java/org/openapitools/model/NullableClassDto.java @@ -97,7 +97,7 @@ public NullableClassDto numberProp(BigDecimal numberProp) { * Get numberProp * @return numberProp */ - @Valid + @Schema(name = "number_prop", requiredMode = Schema.RequiredMode.NOT_REQUIRED, nullable = true) @JsonProperty("number_prop") public JsonNullable getNumberProp() { @@ -157,7 +157,7 @@ public NullableClassDto dateProp(LocalDate dateProp) { * Get dateProp * @return dateProp */ - @Valid + @Schema(name = "date_prop", requiredMode = Schema.RequiredMode.NOT_REQUIRED, nullable = true) @JsonProperty("date_prop") public JsonNullable getDateProp() { @@ -177,7 +177,7 @@ public NullableClassDto datetimeProp(OffsetDateTime datetimeProp) { * Get datetimeProp * @return datetimeProp */ - @Valid + @Schema(name = "datetime_prop", requiredMode = Schema.RequiredMode.NOT_REQUIRED, nullable = true) @JsonProperty("datetime_prop") public JsonNullable getDatetimeProp() {