Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,14 @@ public void processOpts() {

additionalProperties.put("lambdaSplitString", new SplitStringLambda());

additionalProperties.put("lambdaBeanValidationType", (Mustache.Lambda) (fragment, writer) -> {
String type = fragment.execute();
int genericStart = type.indexOf('<');
int qualifierEnd = (genericStart < 0 ? type : type.substring(0, genericStart)).lastIndexOf('.');
// Type-use annotations belong after the package qualifier, before the simple type name.
writer.write(type.substring(0, qualifierEnd + 1) + "@Valid " + type.substring(qualifierEnd + 1));
});

// apiController: hide implementation behind undocumented flag to temporarily preserve code
additionalProperties.put("_api_controller_impl_", false);
// HEADS-UP: Do not add more template file after this block
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{^vendorExtensions.x-sse}}{{#reactive}}{{#useResponseEntity}}{{#vendorExtensions.x-reactive-return-except-list-of-string}}Mono<ResponseEntity<{{{returnContainer}}}<{{>returnTypes}}>>>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{^vendorExtensions.x-reactive-return-except-list-of-string}}Mono<ResponseEntity<{{#isArray}}Flux<{{/isArray}}{{>returnTypes}}{{#isArray}}>{{/isArray}}>>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{/useResponseEntity}}{{^useResponseEntity}}{{#vendorExtensions.x-reactive-return-except-list-of-string}}Mono<{{{returnContainer}}}<{{>returnTypes}}>>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{^vendorExtensions.x-reactive-return-except-list-of-string}}{{#isArray}}Flux{{/isArray}}{{^isArray}}Mono{{/isArray}}<{{>returnTypes}}>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{/useResponseEntity}}{{/reactive}}{{^reactive}}{{#responseWrapper}}{{.}}<{{/responseWrapper}}{{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{>returnTypes}}{{#useResponseEntity}}>{{/useResponseEntity}}{{#responseWrapper}}>{{/responseWrapper}}{{/reactive}}{{/vendorExtensions.x-sse}}{{#vendorExtensions.x-sse}}{{#vendorExtensions.x-reactive-return-except-list-of-string}}{{{returnContainer}}}<{{>returnTypes}}>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{^vendorExtensions.x-reactive-return-except-list-of-string}}{{#isArray}}Flux{{/isArray}}{{^isArray}}Mono{{/isArray}}<{{>returnTypes}}>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{/vendorExtensions.x-sse}}
{{#useBeanValidation}}{{^isVoid}}@Valid {{/isVoid}}{{/useBeanValidation}}{{^vendorExtensions.x-sse}}{{#reactive}}{{#useResponseEntity}}{{#vendorExtensions.x-reactive-return-except-list-of-string}}Mono<ResponseEntity<{{{returnContainer}}}<{{>returnTypes}}>>>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{^vendorExtensions.x-reactive-return-except-list-of-string}}Mono<ResponseEntity<{{#isArray}}Flux<{{/isArray}}{{>returnTypes}}{{#isArray}}>{{/isArray}}>>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{/useResponseEntity}}{{^useResponseEntity}}{{#vendorExtensions.x-reactive-return-except-list-of-string}}Mono<{{{returnContainer}}}<{{>returnTypes}}>>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{^vendorExtensions.x-reactive-return-except-list-of-string}}{{#isArray}}Flux{{/isArray}}{{^isArray}}Mono{{/isArray}}<{{>returnTypes}}>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{/useResponseEntity}}{{/reactive}}{{^reactive}}{{#responseWrapper}}{{.}}<{{/responseWrapper}}{{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{>returnTypes}}{{#useResponseEntity}}>{{/useResponseEntity}}{{#responseWrapper}}>{{/responseWrapper}}{{/reactive}}{{/vendorExtensions.x-sse}}{{#vendorExtensions.x-sse}}{{#vendorExtensions.x-reactive-return-except-list-of-string}}{{{returnContainer}}}<{{>returnTypes}}>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{^vendorExtensions.x-reactive-return-except-list-of-string}}{{#isArray}}Flux{{/isArray}}{{^isArray}}Mono{{/isArray}}<{{>returnTypes}}>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{/vendorExtensions.x-sse}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The {{^isVoid}} guard does not exclude primitive/scalar return types, so this change contradicts the PR description's claim that "Primitive and void responses remain unannotated". SpringCodegen.doDataTypeAssignment (SpringCodegen.java:1320) sets isVoid only when returnType == null, so operations whose response is integer, boolean, number, etc. (Java Integer, Long, Boolean, BigDecimal) will now be generated as @Valid Integer getX() etc. When return-value validation actually runs (e.g., the interface is used with method validation / @Validated), Hibernate Validator requires a value extractor for the cascaded scalar type and throws ConstraintDeclarationException ("No value extractor found for type") — the same breakage the PR is trying to avoid for wrappers. Exclude primitives with the operation flag; note beanValidation.mustache already uses the {{^isPrimitiveType}} pattern for exactly this reason.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At modules/openapi-generator/src/main/resources/JavaSpring/responseType.mustache, line 1:

<comment>The `{{^isVoid}}` guard does not exclude primitive/scalar return types, so this change contradicts the PR description's claim that "Primitive and void responses remain unannotated". `SpringCodegen.doDataTypeAssignment` (SpringCodegen.java:1320) sets `isVoid` only when `returnType == null`, so operations whose response is `integer`, `boolean`, `number`, etc. (Java `Integer`, `Long`, `Boolean`, `BigDecimal`) will now be generated as `@Valid Integer getX()` etc. When return-value validation actually runs (e.g., the interface is used with method validation / `@Validated`), Hibernate Validator requires a value extractor for the cascaded scalar type and throws `ConstraintDeclarationException` ("No value extractor found for type") — the same breakage the PR is trying to avoid for wrappers. Exclude primitives with the operation flag; note `beanValidation.mustache` already uses the `{{^isPrimitiveType}}` pattern for exactly this reason.</comment>

<file context>
@@ -1 +1 @@
-{{^vendorExtensions.x-sse}}{{#reactive}}{{#useResponseEntity}}{{#vendorExtensions.x-reactive-return-except-list-of-string}}Mono<ResponseEntity<{{{returnContainer}}}<{{>returnTypes}}>>>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{^vendorExtensions.x-reactive-return-except-list-of-string}}Mono<ResponseEntity<{{#isArray}}Flux<{{/isArray}}{{>returnTypes}}{{#isArray}}>{{/isArray}}>>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{/useResponseEntity}}{{^useResponseEntity}}{{#vendorExtensions.x-reactive-return-except-list-of-string}}Mono<{{{returnContainer}}}<{{>returnTypes}}>>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{^vendorExtensions.x-reactive-return-except-list-of-string}}{{#isArray}}Flux{{/isArray}}{{^isArray}}Mono{{/isArray}}<{{>returnTypes}}>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{/useResponseEntity}}{{/reactive}}{{^reactive}}{{#responseWrapper}}{{.}}<{{/responseWrapper}}{{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{>returnTypes}}{{#useResponseEntity}}>{{/useResponseEntity}}{{#responseWrapper}}>{{/responseWrapper}}{{/reactive}}{{/vendorExtensions.x-sse}}{{#vendorExtensions.x-sse}}{{#vendorExtensions.x-reactive-return-except-list-of-string}}{{{returnContainer}}}<{{>returnTypes}}>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{^vendorExtensions.x-reactive-return-except-list-of-string}}{{#isArray}}Flux{{/isArray}}{{^isArray}}Mono{{/isArray}}<{{>returnTypes}}>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{/vendorExtensions.x-sse}}
\ No newline at end of file
+{{#useBeanValidation}}{{^isVoid}}@Valid {{/isVoid}}{{/useBeanValidation}}{{^vendorExtensions.x-sse}}{{#reactive}}{{#useResponseEntity}}{{#vendorExtensions.x-reactive-return-except-list-of-string}}Mono<ResponseEntity<{{{returnContainer}}}<{{>returnTypes}}>>>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{^vendorExtensions.x-reactive-return-except-list-of-string}}Mono<ResponseEntity<{{#isArray}}Flux<{{/isArray}}{{>returnTypes}}{{#isArray}}>{{/isArray}}>>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{/useResponseEntity}}{{^useResponseEntity}}{{#vendorExtensions.x-reactive-return-except-list-of-string}}Mono<{{{returnContainer}}}<{{>returnTypes}}>>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{^vendorExtensions.x-reactive-return-except-list-of-string}}{{#isArray}}Flux{{/isArray}}{{^isArray}}Mono{{/isArray}}<{{>returnTypes}}>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{/useResponseEntity}}{{/reactive}}{{^reactive}}{{#responseWrapper}}{{.}}<{{/responseWrapper}}{{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{>returnTypes}}{{#useResponseEntity}}>{{/useResponseEntity}}{{#responseWrapper}}>{{/responseWrapper}}{{/reactive}}{{/vendorExtensions.x-sse}}{{#vendorExtensions.x-sse}}{{#vendorExtensions.x-reactive-return-except-list-of-string}}{{{returnContainer}}}<{{>returnTypes}}>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{^vendorExtensions.x-reactive-return-except-list-of-string}}{{#isArray}}Flux{{/isArray}}{{^isArray}}Mono{{/isArray}}<{{>returnTypes}}>{{/vendorExtensions.x-reactive-return-except-list-of-string}}{{/vendorExtensions.x-sse}}
\ No newline at end of file
</file context>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Can you add a test proving what you have stated? I would like to see the situation causing ConstraintDeclarationException which you have described.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The existing tests assert generated signatures but do not reproduce runtime validation. I can’t verify the claimed ConstraintDeclarationException from this diff alone; a test exercising method return-value validation for a scalar response would demonstrate whether the finding applies.

Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#isMap}}Map<String, {{{returnType}}}>{{/isMap}}{{#isArray}}{{#reactive}}{{{returnType}}}{{/reactive}}{{^reactive}}{{#generateGenericResponseEntity}}?{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{{returnContainer}}}<{{{returnType}}}>{{/generateGenericResponseEntity}}{{/reactive}}{{/isArray}}{{^returnContainer}}{{#useResponseEntity}}{{#generateGenericResponseEntity}}?{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{{returnType}}}{{/generateGenericResponseEntity}}{{/useResponseEntity}}{{^useResponseEntity}}{{#isDelegate}}{{#isVoid}}{{#responseWrapper}}{{{returnType}}}{{/responseWrapper}}{{^responseWrapper}}void{{/responseWrapper}}{{/isVoid}}{{^isVoid}}{{{returnType}}}{{/isVoid}}{{/isDelegate}}{{^isDelegate}}{{#async}}{{{returnType}}}{{/async}}{{^async}}{{#isVoid}}{{#responseWrapper}}{{{returnType}}}{{/responseWrapper}}{{^responseWrapper}}void{{/responseWrapper}}{{/isVoid}}{{^isVoid}}{{{returnType}}}{{/isVoid}}{{/async}}{{/isDelegate}}{{/useResponseEntity}}{{/returnContainer}}
{{#isMap}}Map<String, {{>validatedReturnType}}>{{/isMap}}{{#isArray}}{{#reactive}}{{{returnType}}}{{/reactive}}{{^reactive}}{{#generateGenericResponseEntity}}?{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{{returnContainer}}}<{{>validatedReturnType}}>{{/generateGenericResponseEntity}}{{/reactive}}{{/isArray}}{{^returnContainer}}{{#useResponseEntity}}{{#generateGenericResponseEntity}}?{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{{returnType}}}{{/generateGenericResponseEntity}}{{/useResponseEntity}}{{^useResponseEntity}}{{#isDelegate}}{{#isVoid}}{{#responseWrapper}}{{{returnType}}}{{/responseWrapper}}{{^responseWrapper}}void{{/responseWrapper}}{{/isVoid}}{{^isVoid}}{{{returnType}}}{{/isVoid}}{{/isDelegate}}{{^isDelegate}}{{#async}}{{{returnType}}}{{/async}}{{^async}}{{#isVoid}}{{#responseWrapper}}{{{returnType}}}{{/responseWrapper}}{{^responseWrapper}}void{{/responseWrapper}}{{/isVoid}}{{^isVoid}}{{{returnType}}}{{/isVoid}}{{/async}}{{/isDelegate}}{{/useResponseEntity}}{{/returnContainer}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Map values are annotated even when the response is wrapped in ResponseEntity, producing ResponseEntity<Map<String, @Valid Pet>> and requiring the ResponseEntity value extractor the PR intends to avoid. Suppress map-value @Valid for useResponseEntity returns.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At modules/openapi-generator/src/main/resources/JavaSpring/returnTypes.mustache, line 1:

<comment>Map values are annotated even when the response is wrapped in `ResponseEntity`, producing `ResponseEntity<Map<String, @Valid Pet>>` and requiring the `ResponseEntity` value extractor the PR intends to avoid. Suppress map-value `@Valid` for `useResponseEntity` returns.</comment>

<file context>
@@ -1 +1 @@
-{{#isMap}}Map<String, {{{returnType}}}>{{/isMap}}{{#isArray}}{{#reactive}}{{{returnType}}}{{/reactive}}{{^reactive}}{{#generateGenericResponseEntity}}?{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{{returnContainer}}}<{{{returnType}}}>{{/generateGenericResponseEntity}}{{/reactive}}{{/isArray}}{{^returnContainer}}{{#useResponseEntity}}{{#generateGenericResponseEntity}}?{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{{returnType}}}{{/generateGenericResponseEntity}}{{/useResponseEntity}}{{^useResponseEntity}}{{#isDelegate}}{{#isVoid}}{{#responseWrapper}}{{{returnType}}}{{/responseWrapper}}{{^responseWrapper}}void{{/responseWrapper}}{{/isVoid}}{{^isVoid}}{{{returnType}}}{{/isVoid}}{{/isDelegate}}{{^isDelegate}}{{#async}}{{{returnType}}}{{/async}}{{^async}}{{#isVoid}}{{#responseWrapper}}{{{returnType}}}{{/responseWrapper}}{{^responseWrapper}}void{{/responseWrapper}}{{/isVoid}}{{^isVoid}}{{{returnType}}}{{/isVoid}}{{/async}}{{/isDelegate}}{{/useResponseEntity}}{{/returnContainer}}
\ No newline at end of file
+{{#isMap}}Map<String, {{>validatedReturnType}}>{{/isMap}}{{#isArray}}{{#reactive}}{{{returnType}}}{{/reactive}}{{^reactive}}{{#generateGenericResponseEntity}}?{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{{returnContainer}}}<{{>validatedReturnType}}>{{/generateGenericResponseEntity}}{{/reactive}}{{/isArray}}{{^returnContainer}}{{#useResponseEntity}}{{#generateGenericResponseEntity}}?{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{{returnType}}}{{/generateGenericResponseEntity}}{{/useResponseEntity}}{{^useResponseEntity}}{{#isDelegate}}{{#isVoid}}{{#responseWrapper}}{{{returnType}}}{{/responseWrapper}}{{^responseWrapper}}void{{/responseWrapper}}{{/isVoid}}{{^isVoid}}{{{returnType}}}{{/isVoid}}{{/isDelegate}}{{^isDelegate}}{{#async}}{{{returnType}}}{{/async}}{{^async}}{{#isVoid}}{{#responseWrapper}}{{{returnType}}}{{/responseWrapper}}{{^responseWrapper}}void{{/responseWrapper}}{{/isVoid}}{{^isVoid}}{{{returnType}}}{{/isVoid}}{{/async}}{{/isDelegate}}{{/useResponseEntity}}{{/returnContainer}}
\ No newline at end of file
</file context>

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#useBeanValidation}}{{#returnProperty.items.isModel}}{{#lambdaBeanValidationType}}{{{returnType}}}{{/lambdaBeanValidationType}}{{/returnProperty.items.isModel}}{{^returnProperty.items.isModel}}{{{returnType}}}{{/returnProperty.items.isModel}}{{/useBeanValidation}}{{^useBeanValidation}}{{{returnType}}}{{/useBeanValidation}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Nested collection responses skip cascade validation because this branch checks only the immediate item. Apply the model annotation recursively so a List<List<Pet>> response annotates its Pet elements.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At modules/openapi-generator/src/main/resources/JavaSpring/validatedReturnType.mustache, line 1:

<comment>Nested collection responses skip cascade validation because this branch checks only the immediate item. Apply the model annotation recursively so a `List<List<Pet>>` response annotates its `Pet` elements.</comment>

<file context>
@@ -0,0 +1 @@
+{{#useBeanValidation}}{{#returnProperty.items.isModel}}{{#lambdaBeanValidationType}}{{{returnType}}}{{/lambdaBeanValidationType}}{{/returnProperty.items.isModel}}{{^returnProperty.items.isModel}}{{{returnType}}}{{/returnProperty.items.isModel}}{{/useBeanValidation}}{{^useBeanValidation}}{{{returnType}}}{{/useBeanValidation}}
\ No newline at end of file
</file context>

Original file line number Diff line number Diff line change
Expand Up @@ -464,21 +464,20 @@ public void typeMappingsForCollections(Map<String, String> typeMappings, String

JavaFileAssert.assertThat(files.get("ListOfObjectsApi.java"))
.fileContains(
"ResponseEntity<" + arrayMapping + "<ListOfObjectsInner>>",
"ResponseEntity<" + arrayMapping + "<@Valid ListOfObjectsInner>>",
arrayMapping + "<@Valid ListOfObjectsInner> listOfObjectsInner")
.fileContainsPattern("@Size\\(min = 5\\)\\s*@RequestBody");

String myItem = typeMappings.getOrDefault("MyItem", "MyItem");
JavaFileAssert.assertThat(files.get("ListOfQualifiedItemApi.java"))
.fileContains(
"ResponseEntity<" + arrayMapping + "<" + myItem + ">>",
"ResponseEntity<" + arrayMapping + "<" + expectedMyItemArgument + ">>",
arrayMapping + "<" + expectedMyItemArgument + ">");

if (!typeMappings.containsKey("array")) {
// the mapping to Set is done automatically with uniqueItems: true
JavaFileAssert.assertThat(files.get("ListOfUniqueItemApi.java"))
.fileContains(
"ResponseEntity<Set<" + myItem + ">>",
"ResponseEntity<Set<" + expectedMyItemArgument + ">>",
"Set<" + expectedMyItemArgument + "> ");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2817,15 +2817,15 @@ public void testResponseWithArray_issue11897() throws Exception {
Map<String, File> files = generateFromContract("src/test/resources/bugs/issue_11897.yaml", SPRING_BOOT, additionalProperties);

JavaFileAssert.assertThat(files.get("MetadataApi.java"))
.assertMethod("getWithArrayOfObjects").hasReturnType("ResponseEntity<List<TestResponse>>")
.assertMethod("getWithArrayOfObjects").hasReturnType("ResponseEntity<List<@Valid TestResponse>>")
.toFileAssert()
.assertMethod("getWithArrayOfString").hasReturnType("ResponseEntity<List<String>>")
.toFileAssert()
.assertMethod("getWithSetOfObjects").hasReturnType("ResponseEntity<Set<TestResponse>>")
.assertMethod("getWithSetOfObjects").hasReturnType("ResponseEntity<Set<@Valid TestResponse>>")
.toFileAssert()
.assertMethod("getWithSetOfStrings").hasReturnType("ResponseEntity<Set<String>>")
.toFileAssert()
.assertMethod("getWithMapOfObjects").hasReturnType("ResponseEntity<Map<String, TestResponse>>")
.assertMethod("getWithMapOfObjects").hasReturnType("ResponseEntity<Map<String, @Valid TestResponse>>")
.toFileAssert()
.assertMethod("getWithMapOfStrings").hasReturnType("ResponseEntity<Map<String, String>>");
}
Expand Down Expand Up @@ -3455,6 +3455,90 @@ public void useBeanValidationGenerateAnnotationsForRequestBody_issue13932() thro
.containsWithNameAndAttributes("Min", ImmutableMap.of("value", "2"));
}

@DataProvider
public Object[][] collectionResponseValidationOptions() {
return new Object[][] {
{true, false, true, false}, {false, false, true, false},
{true, false, false, false}, {false, false, false, false},
{true, true, true, false}, {false, true, true, false},
{true, true, false, false}, {false, true, false, false},
{true, false, true, true}, {false, false, true, true},
{true, false, false, true}, {false, false, false, true},
{true, true, true, true}, {false, true, true, true},
{true, true, false, true}, {false, true, false, true}
};
}

@Test(dataProvider = "collectionResponseValidationOptions")
public void validatesNonVoidReturnsAndObjectCollectionElements(boolean validation, boolean reactive,
boolean responseEntity, boolean mappedModel) throws IOException {
OpenAPI openAPI = new OpenAPIParser().readLocation(
"src/test/resources/3_0/spring/collection-response-validation.yaml", null, new ParseOptions()).getOpenAPI();
SpringCodegen codegen = new SpringCodegen();
codegen.setOutputDir(Files.createTempDirectory("collection-response-validation").toString());
codegen.additionalProperties().put(USE_TAGS, true);
codegen.additionalProperties().put(INTERFACE_ONLY, true);
codegen.additionalProperties().put(USE_BEANVALIDATION, validation);
codegen.additionalProperties().put(REACTIVE, reactive);
codegen.additionalProperties().put(USE_RESPONSE_ENTITY, responseEntity);
if (mappedModel) {
codegen.schemaMapping().put("Pet", "com.example.ExternalPet");
}

DefaultGenerator generator = new DefaultGenerator();
generator.setGenerateMetadata(false);
Map<String, File> files = generator.opts(new ClientOptInput().openAPI(openAPI).config(codegen))
.generate().stream().collect(Collectors.toMap(File::getName, Function.identity()));
String api = Files.readString(files.get("CollectionApi.java").toPath());
String model = mappedModel ? "com.example.ExternalPet" : "Pet";
String element = validation ? (mappedModel ? "com.example.@Valid ExternalPet" : "@Valid Pet") : model;
String list = reactive ? "Flux<" + model + ">" : "List<" + element + ">";
String set = reactive ? "Flux<" + model + ">" : "Set<" + element + ">";
String map = "Map<String, " + element + ">";
String strings = "List<String>";
String integers = reactive ? "Flux<Integer>" : "List<Integer>";
String stringMap = "Map<String, String>";
String pet = model;
String name = "String";
String noContent = responseEntity ? "ResponseEntity<Void>" : (reactive ? "Void" : "void");
if (responseEntity) {
list = "ResponseEntity<" + list + ">";
set = "ResponseEntity<" + set + ">";
map = "ResponseEntity<" + map + ">";
strings = "ResponseEntity<" + strings + ">";
integers = "ResponseEntity<" + integers + ">";
stringMap = "ResponseEntity<" + stringMap + ">";
pet = "ResponseEntity<" + pet + ">";
name = "ResponseEntity<" + name + ">";
}
if (reactive) {
if (responseEntity) {
list = "Mono<" + list + ">";
set = "Mono<" + set + ">";
integers = "Mono<" + integers + ">";
}
map = "Mono<" + map + ">";
strings = "Mono<" + strings + ">";
stringMap = "Mono<" + stringMap + ">";
pet = "Mono<" + pet + ">";
name = "Mono<" + name + ">";
noContent = "Mono<" + noContent + ">";
}
String annotation = validation ? "@Valid " : "";
assertThat(api).contains(annotation + list + " getPets(", annotation + set + " getUniquePets(",
annotation + map + " getPetsByName(", annotation + strings + " getNames(",
annotation + pet + " getPet(", annotation + integers + " getNumbers(",
annotation + stringMap + " getNamesById(", annotation + name + " getName(",
"default " + noContent + " getNothing(")
.doesNotContain("ResponseEntity<@Valid", "Mono<@Valid", "List<@Valid String", "Flux<@Valid",
"Flux<com.example.@Valid",
"ResponseEntity<com.example.@Valid", "Mono<com.example.@Valid",
"@Valid " + noContent + " getNothing(");
if (!validation) {
assertThat(api).doesNotContain("@Valid");
}
}

@Test
public void useBeanValidationGenerateAnnotationsForFormsRequestBody() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
Expand Down Expand Up @@ -6586,7 +6670,7 @@ public void testGenericReturnTypeWhenNotUsingResponseEntity_issue1096() throws I
JavaFileAssert.assertThat(files.get("PetApi.java"))
.assertMethod("getPetById").hasReturnType("Pet")
.toFileAssert()
.assertMethod("findPetsByStatus").hasReturnType("List<Pet>");
.assertMethod("findPetsByStatus").hasReturnType("List<@Valid Pet>");
}

@Test
Expand Down Expand Up @@ -8634,7 +8718,7 @@ public void substituteGenericPagedModel_doesNotReplaceNonPagedReturnType() throw

JavaFileAssert.assertThat(files.get("PetApi.java"))
.assertMethod("findPets")
.hasReturnType("ResponseEntity<List<Pet>>");
.hasReturnType("ResponseEntity<List<@Valid Pet>>");
}

/** Common properties shared by all substituteGenericPagedModel tests. */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
openapi: 3.0.3
info:
title: Collection response validation
version: 1.0.0
paths:
/pets:
get:
operationId: getPets
tags: [collection]
responses:
'200':
description: Pets
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
/unique-pets:
get:
operationId: getUniquePets
tags: [collection]
responses:
'200':
description: Unique pets
content:
application/json:
schema:
type: array
uniqueItems: true
items:
$ref: '#/components/schemas/Pet'
/pets-by-name:
get:
operationId: getPetsByName
tags: [collection]
responses:
'200':
description: Pets by name
content:
application/json:
schema:
type: object
additionalProperties:
$ref: '#/components/schemas/Pet'
/names:
get:
operationId: getNames
tags: [collection]
responses:
'200':
description: Names
content:
application/json:
schema:
type: array
items:
type: string
/numbers:
get:
operationId: getNumbers
tags: [collection]
responses:
'200':
description: Numbers
content:
application/json:
schema:
type: array
items:
type: integer
/names-by-id:
get:
operationId: getNamesById
tags: [collection]
responses:
'200':
description: Names by ID
content:
application/json:
schema:
type: object
additionalProperties:
type: string
/pet:
get:
operationId: getPet
tags: [collection]
responses:
'200':
description: Pet
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
/name:
get:
operationId: getName
tags: [collection]
responses:
'200':
description: Name
content:
application/json:
schema:
type: string
/nothing:
get:
operationId: getNothing
tags: [collection]
responses:
'204':
description: No content
components:
schemas:
Pet:
type: object
properties:
name:
type: string
Loading