Skip to content

Commit 86dc86b

Browse files
committed
[jaxrs-spec][quarkus] add @ResponseStatus annotation for non-200 success codes
Adds @org.jboss.resteasy.reactive.ResponseStatus to generated Quarkus JAX-RS server stubs when the OpenAPI spec defines a non-200 success response code (2xx). This allows Quarkus to automatically set the correct HTTP status code without the user manually returning a Response object. Gated on useJakartaEe: true (Quarkus 3+ / Jakarta EE namespace) because @ResponseStatus is a RESTEasy Reactive feature, which is the default REST stack only in Quarkus 3+. Quarkus 1.x/2.x projects use quarkus-resteasy (classic) which does not process this annotation, and the quarkus-universe-bom 1.x provides a version of resteasy-reactive that predates ResponseStatus. - JavaJAXRSSpecServerCodegen: set x-java-success-response-code vendor extension for Quarkus+Jakarta EE+non-Response-returning operations - quarkus/apiMethod.mustache, api.mustache, apiInterface.mustache: emit @ResponseStatus(<code>) when the vendor extension is present - quarkus/pom.mustache: add resteasy-reactive dep when annotation is used - Tests and generated samples updated
1 parent 2917ce8 commit 86dc86b

16 files changed

Lines changed: 373 additions & 14 deletions

File tree

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,23 @@ public ModelsMap postProcessModels(ModelsMap objs) {
343343
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> allModels) {
344344
objs = super.postProcessOperationsWithModels(objs, allModels);
345345
removeImport(objs, "java.util.List");
346+
if (QUARKUS_LIBRARY.equals(library) && !returnResponse && !returnJbossResponse && useJakartaEe) {
347+
for (CodegenOperation op : objs.getOperations().getOperation()) {
348+
op.responses.stream()
349+
.filter(r -> r.is2xx && r.code.matches("\\d+"))
350+
.findFirst()
351+
.ifPresent(r -> op.vendorExtensions.put("x-java-success-response-code", r.code));
352+
}
353+
boolean hasAnnotations = objs.getOperations().getOperation().stream()
354+
.anyMatch(op -> op.vendorExtensions.containsKey("x-java-success-response-code"));
355+
// Always set explicitly so Mustache does not fall through to the global additionalProperties
356+
// value when this file has no annotated operations.
357+
objs.put("hasResponseStatusAnnotations", hasAnnotations);
358+
if (hasAnnotations) {
359+
// Global flag used by pom.mustache, which is rendered once per project.
360+
additionalProperties.put("hasResponseStatusAnnotations", true);
361+
}
362+
}
346363
return objs;
347364
}
348365

modules/openapi-generator/src/main/resources/JavaJaxRS/spec/libraries/quarkus/api.mustache

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ package {{package}};
66
import {{javaxPackage}}.ws.rs.*;
77
import {{javaxPackage}}.ws.rs.core.Response;
88
{{#returnJBossResponse}}import org.jboss.resteasy.reactive.RestResponse;{{/returnJBossResponse}}
9-
9+
{{#hasResponseStatusAnnotations}}import org.jboss.resteasy.reactive.ResponseStatus;{{/hasResponseStatusAnnotations}}
1010
{{#useGzipFeature}}
1111
import org.jboss.resteasy.annotations.GZIP;
1212
{{/useGzipFeature}}
@@ -116,4 +116,4 @@ public {{#interfaceOnly}}interface{{/interfaceOnly}}{{^interfaceOnly}}class{{/in
116116
{{#interfaceOnly}}{{>apiInterface}}{{/interfaceOnly}}{{^interfaceOnly}}{{>apiMethod}}{{/interfaceOnly}}
117117
{{/operation}}
118118
}
119-
{{/operations}}
119+
{{/operations}}

modules/openapi-generator/src/main/resources/JavaJaxRS/spec/libraries/quarkus/apiInterface.mustache

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,5 @@
5050
{{^vendorExtensions.x-java-is-response-void}}@org.eclipse.microprofile.openapi.annotations.media.Content(schema = @org.eclipse.microprofile.openapi.annotations.media.Schema(implementation = {{{baseType}}}.class{{#vendorExtensions.x-microprofile-open-api-return-schema-container}}, type = {{{.}}} {{/vendorExtensions.x-microprofile-open-api-return-schema-container}}{{#vendorExtensions.x-microprofile-open-api-return-unique-items}}, uniqueItems = true {{/vendorExtensions.x-microprofile-open-api-return-unique-items}})){{/vendorExtensions.x-java-is-response-void}}
5151
}){{^-last}},{{/-last}}{{/responses}}
5252
}){{/hasProduces}}{{/useMicroProfileOpenAPIAnnotations}}
53-
{{#supportAsync}}{{>returnAsyncTypeInterface}}{{/supportAsync}}{{^supportAsync}}{{#returnJBossResponse}}{{>returnResponseTypeInterface}}{{/returnJBossResponse}}{{^returnJBossResponse}}{{#returnResponse}}Response{{/returnResponse}}{{^returnResponse}}{{>returnTypeInterface}}{{/returnResponse}}{{/returnJBossResponse}}{{/supportAsync}} {{nickname}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>cookieParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}});
53+
{{#vendorExtensions.x-java-success-response-code}}@ResponseStatus({{{vendorExtensions.x-java-success-response-code}}}){{/vendorExtensions.x-java-success-response-code}}
54+
{{#supportAsync}}{{>returnAsyncTypeInterface}}{{/supportAsync}}{{^supportAsync}}{{#returnJBossResponse}}{{>returnResponseTypeInterface}}{{/returnJBossResponse}}{{^returnJBossResponse}}{{#returnResponse}}Response{{/returnResponse}}{{^returnResponse}}{{>returnTypeInterface}}{{/returnResponse}}{{/returnJBossResponse}}{{/supportAsync}} {{nickname}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>cookieParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}});

modules/openapi-generator/src/main/resources/JavaJaxRS/spec/libraries/quarkus/apiMethod.mustache

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
{{^vendorExtensions.x-java-is-response-void}}@org.eclipse.microprofile.openapi.annotations.media.Content(schema = @org.eclipse.microprofile.openapi.annotations.media.Schema(implementation = {{{baseType}}}.class{{#vendorExtensions.x-microprofile-open-api-return-schema-container}}, type = {{{.}}} {{/vendorExtensions.x-microprofile-open-api-return-schema-container}}{{#vendorExtensions.x-microprofile-open-api-return-unique-items}}, uniqueItems = true {{/vendorExtensions.x-microprofile-open-api-return-unique-items}})){{/vendorExtensions.x-java-is-response-void}}
4848
}){{^-last}},{{/-last}}{{/responses}}
4949
}){{/hasProduces}}{{/useMicroProfileOpenAPIAnnotations}}
50+
{{#vendorExtensions.x-java-success-response-code}}@ResponseStatus({{{vendorExtensions.x-java-success-response-code}}}){{/vendorExtensions.x-java-success-response-code}}
5051
public {{#supportAsync}}{{#useMutiny}}Uni{{/useMutiny}}{{^useMutiny}}CompletionStage{{/useMutiny}}<{{/supportAsync}}{{#returnJBossResponse}}{{>returnResponseTypeInterface}}{{/returnJBossResponse}}{{^returnJBossResponse}}Response{{/returnJBossResponse}}{{#supportAsync}}>{{/supportAsync}} {{nickname}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>cookieParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}) {
5152
return {{#supportAsync}}{{#useMutiny}}Uni.createFrom().item({{/useMutiny}}{{^useMutiny}}CompletableFuture.supplyAsync(() -> {{/useMutiny}}{{/supportAsync}}Response.ok().entity("magic!").build(){{#supportAsync}}){{/supportAsync}};
52-
}
53+
}

modules/openapi-generator/src/main/resources/JavaJaxRS/spec/libraries/quarkus/pom.mustache

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@
8484
<groupId>io.quarkus</groupId>
8585
<artifactId>quarkus-smallrye-openapi</artifactId>
8686
</dependency>
87+
{{#hasResponseStatusAnnotations}}
88+
<dependency>
89+
<groupId>io.quarkus.resteasy.reactive</groupId>
90+
<artifactId>resteasy-reactive</artifactId>
91+
</dependency>
92+
{{/hasResponseStatusAnnotations}}
8793
{{#useJakartaEe}}
8894
{{#returnJBossResponse}}
8995
<dependency>

modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecServerCodegenTest.java

Lines changed: 212 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,7 @@ public void generateApiWithAsyncSupportAndInterfaceOnlyAndJBossResponse() throws
552552
//And the generated interface contains CompletionStage<RestResponse<Pet>>
553553
TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/PetApi.java");
554554
assertFileContains(output.toPath().resolve("src/gen/java/org/openapitools/api/PetApi.java"),
555-
"\nimport org.jboss.resteasy.reactive.RestResponse;\n",
555+
"\nimport org.jboss.resteasy.reactive.RestResponse;\n",
556556
"\nimport java.util.concurrent.CompletionStage;\n",
557557
"CompletionStage<RestResponse<Pet>> addPet", "CompletionStage<RestResponse<Void>> deletePet");
558558
}
@@ -1236,7 +1236,7 @@ public void disableGenerateJsonCreator() throws Exception {
12361236

12371237
assertFileNotContains(files.get("RequiredProperties.java").toPath(), "@JsonCreator");
12381238
}
1239-
1239+
12401240
@Test
12411241
public void testDiscriminatorMappingUsedInJsonTypeName() throws Exception {
12421242
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
@@ -1273,37 +1273,37 @@ public void testDiscriminatorMappingUsedInJsonTypeName() throws Exception {
12731273
public void testGenerateJsonNullableListFieldsHelperMethodReferences_issue23251() throws Exception {
12741274
Map<String, Object> properties = new HashMap<>();
12751275
properties.put(OPENAPI_NULLABLE, "true");
1276-
1276+
12771277
File output = Files.createTempDirectory("test").toFile();
1278-
1278+
12791279
final CodegenConfigurator configurator = new CodegenConfigurator()
12801280
.setGeneratorName("jaxrs-spec")
12811281
.setAdditionalProperties(properties)
12821282
.setInputSpec("src/test/resources/bugs/issue_23251.yaml")
12831283
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));
1284-
1284+
12851285
final ClientOptInput clientOptInput = configurator.toClientOptInput();
12861286
DefaultGenerator generator = new DefaultGenerator();
12871287
List<File> files = generator.opts(clientOptInput).generate();
1288-
1288+
12891289
validateJavaSourceFiles(files);
1290-
1290+
12911291
TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/model/BugResponse.java");
1292-
1292+
12931293
// Assert that the generated model contains JsonNullable fields
12941294
assertFileContains(output.toPath().resolve("src/gen/java/org/openapitools/model/BugResponse.java"),
12951295
"private JsonNullable<String> nullableField = JsonNullable.<String>undefined();",
12961296
"private JsonNullable<List<String>> nullableList = JsonNullable.<List<String>>undefined();",
12971297
"private JsonNullable<List<@Valid NestedResponse>> nullableObjectList = JsonNullable.<List<@Valid NestedResponse>>undefined();"
12981298
);
1299-
1299+
13001300
// Assert that the generated model contains correct add and remove helper methods reference for JsonNullable fields
13011301
assertFileContains(output.toPath().resolve("src/gen/java/org/openapitools/model/BugResponse.java"),
13021302
"this.nullableList.get().add(nullableListItem);",
13031303
"this.nullableList.get().remove(nullableListItem);",
13041304
"this.nullableObjectList.get().add(nullableObjectListItem);",
13051305
"this.nullableObjectList.get().remove(nullableObjectListItem);");
1306-
1306+
13071307
output.deleteOnExit();
13081308
}
13091309

@@ -1342,4 +1342,206 @@ public void generatesDeprecatedAnnotationsForModelsOperationsAndParameters_issue
13421342
JavaFileAssert.assertThat(petApi).fileContains("findPetsByStatus", "@Deprecated", "@QueryParam(\"status\")");
13431343
}
13441344

1345+
/**
1346+
* Verify that when using the quarkus library with interfaceOnly=true, the generated interface
1347+
* method is always annotated with {@code @ResponseStatus(<code>)} for any 2xx or 3xx response,
1348+
* including 200, for explicit documentation purposes.
1349+
* ping.yaml has a 201 response.
1350+
*/
1351+
@Test
1352+
public void generateQuarkusInterfaceAddsResponseStatusAnnotationForSuccessCode() throws Exception {
1353+
final File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
1354+
output.deleteOnExit();
1355+
1356+
final OpenAPI openAPI = new OpenAPIParser()
1357+
.readLocation("src/test/resources/3_0/ping.yaml", null, new ParseOptions()).getOpenAPI();
1358+
1359+
codegen.setOutputDir(output.getAbsolutePath());
1360+
codegen.setLibrary(QUARKUS_LIBRARY); //Given the quarkus library is used
1361+
codegen.additionalProperties().put(INTERFACE_ONLY, true); //And only interfaces are generated
1362+
codegen.additionalProperties().put(USE_JAKARTA_EE, true); //Required: @ResponseStatus is only emitted for Jakarta EE (Quarkus 3+)
1363+
// returnResponse and returnJBossResponse are both false (defaults)
1364+
1365+
final ClientOptInput input = new ClientOptInput()
1366+
.openAPI(openAPI)
1367+
.config(codegen);
1368+
1369+
final DefaultGenerator generator = new DefaultGenerator();
1370+
final List<File> files = generator.opts(input).generate();
1371+
1372+
validateJavaSourceFiles(files);
1373+
1374+
//Then the generated interface contains the ResponseStatus import and annotation with code 201
1375+
TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/PingApi.java");
1376+
assertFileContains(output.toPath().resolve("src/gen/java/org/openapitools/api/PingApi.java"),
1377+
"import org.jboss.resteasy.reactive.ResponseStatus;",
1378+
"@ResponseStatus(201)");
1379+
}
1380+
1381+
/**
1382+
* Verify that {@code @ResponseStatus(200)} IS emitted even for the default 200 status code,
1383+
* for explicit documentation purposes.
1384+
*/
1385+
@Test
1386+
public void generateQuarkusInterfaceAddsResponseStatusAnnotationFor200Response() throws Exception {
1387+
final File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
1388+
output.deleteOnExit();
1389+
1390+
final OpenAPI openAPI = new OpenAPIParser()
1391+
.readLocation("src/test/resources/3_0/petstore.yaml", null, new ParseOptions()).getOpenAPI();
1392+
1393+
codegen.setOutputDir(output.getAbsolutePath());
1394+
codegen.setLibrary(QUARKUS_LIBRARY);
1395+
codegen.additionalProperties().put(INTERFACE_ONLY, true);
1396+
codegen.additionalProperties().put(USE_JAKARTA_EE, true); //Required: @ResponseStatus is only emitted for Jakarta EE (Quarkus 3+)
1397+
1398+
final ClientOptInput input = new ClientOptInput()
1399+
.openAPI(openAPI)
1400+
.config(codegen);
1401+
1402+
final DefaultGenerator generator = new DefaultGenerator();
1403+
final List<File> files = generator.opts(input).generate();
1404+
1405+
validateJavaSourceFiles(files);
1406+
1407+
//Then @ResponseStatus(200) IS present for explicit documentation
1408+
TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/PetApi.java");
1409+
assertFileContains(output.toPath().resolve("src/gen/java/org/openapitools/api/PetApi.java"),
1410+
"import org.jboss.resteasy.reactive.ResponseStatus;",
1411+
"@ResponseStatus(200)");
1412+
}
1413+
1414+
1415+
/**
1416+
* Verify that the {@code @ResponseStatus} annotation is NOT emitted when returnResponse=true,
1417+
* because the user controls the status code via the {@code Response} builder in that mode.
1418+
*/
1419+
@Test
1420+
public void generateQuarkusInterfaceDoesNotAddResponseStatusAnnotationWhenReturnResponse() throws Exception {
1421+
final File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
1422+
output.deleteOnExit();
1423+
1424+
final OpenAPI openAPI = new OpenAPIParser()
1425+
.readLocation("src/test/resources/3_0/ping.yaml", null, new ParseOptions()).getOpenAPI();
1426+
1427+
codegen.setOutputDir(output.getAbsolutePath());
1428+
codegen.setLibrary(QUARKUS_LIBRARY);
1429+
codegen.additionalProperties().put(INTERFACE_ONLY, true);
1430+
codegen.additionalProperties().put(USE_JAKARTA_EE, true); //Enabled so returnResponse is the only disabling factor
1431+
codegen.additionalProperties().put(RETURN_RESPONSE, true); //Given returnResponse is true
1432+
1433+
final ClientOptInput input = new ClientOptInput()
1434+
.openAPI(openAPI)
1435+
.config(codegen);
1436+
1437+
final DefaultGenerator generator = new DefaultGenerator();
1438+
final List<File> files = generator.opts(input).generate();
1439+
1440+
validateJavaSourceFiles(files);
1441+
1442+
//Then the annotation must NOT appear
1443+
TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/PingApi.java");
1444+
assertFileNotContains(output.toPath().resolve("src/gen/java/org/openapitools/api/PingApi.java"),
1445+
"@ResponseStatus",
1446+
"import org.jboss.resteasy.reactive.ResponseStatus");
1447+
}
1448+
1449+
/**
1450+
* Verify that the {@code @ResponseStatus} annotation is NOT emitted when returnJBossResponse=true,
1451+
* because the caller controls the status code via the {@code RestResponse} wrapper in that mode.
1452+
*/
1453+
@Test
1454+
public void generateQuarkusInterfaceDoesNotAddResponseStatusAnnotationWhenReturnJBossResponse() throws Exception {
1455+
final File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
1456+
output.deleteOnExit();
1457+
1458+
final OpenAPI openAPI = new OpenAPIParser()
1459+
.readLocation("src/test/resources/3_0/ping.yaml", null, new ParseOptions()).getOpenAPI();
1460+
1461+
codegen.setOutputDir(output.getAbsolutePath());
1462+
codegen.setLibrary(QUARKUS_LIBRARY);
1463+
codegen.additionalProperties().put(INTERFACE_ONLY, true);
1464+
codegen.additionalProperties().put(USE_JAKARTA_EE, true); //Required by returnJBossResponse
1465+
codegen.additionalProperties().put(RETURN_JBOSS_RESPONSE, true); //Given returnJBossResponse is true
1466+
1467+
final ClientOptInput input = new ClientOptInput()
1468+
.openAPI(openAPI)
1469+
.config(codegen);
1470+
1471+
final DefaultGenerator generator = new DefaultGenerator();
1472+
final List<File> files = generator.opts(input).generate();
1473+
1474+
validateJavaSourceFiles(files);
1475+
1476+
//Then the annotation must NOT appear
1477+
TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/PingApi.java");
1478+
assertFileNotContains(output.toPath().resolve("src/gen/java/org/openapitools/api/PingApi.java"),
1479+
"@ResponseStatus",
1480+
"import org.jboss.resteasy.reactive.ResponseStatus");
1481+
}
1482+
1483+
/**
1484+
* Verify that {@code @ResponseStatus} is NOT emitted when using a non-Quarkus jaxrs-spec library,
1485+
* since {@code org.jboss.resteasy.reactive.ResponseStatus} is a RESTEasy Reactive / Quarkus-specific annotation.
1486+
*/
1487+
@Test
1488+
public void generateNonQuarkusInterfaceDoesNotAddResponseStatusAnnotation() throws Exception {
1489+
final File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
1490+
output.deleteOnExit();
1491+
1492+
final OpenAPI openAPI = new OpenAPIParser()
1493+
.readLocation("src/test/resources/3_0/ping.yaml", null, new ParseOptions()).getOpenAPI();
1494+
1495+
codegen.setOutputDir(output.getAbsolutePath());
1496+
// No setLibrary call — uses the default jaxrs-spec library
1497+
codegen.additionalProperties().put(INTERFACE_ONLY, true);
1498+
1499+
final ClientOptInput input = new ClientOptInput()
1500+
.openAPI(openAPI)
1501+
.config(codegen);
1502+
1503+
final DefaultGenerator generator = new DefaultGenerator();
1504+
final List<File> files = generator.opts(input).generate();
1505+
1506+
validateJavaSourceFiles(files);
1507+
1508+
//Then the annotation must NOT appear
1509+
TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/PingApi.java");
1510+
assertFileNotContains(output.toPath().resolve("src/gen/java/org/openapitools/api/PingApi.java"),
1511+
"@ResponseStatus",
1512+
"import org.jboss.resteasy.reactive.ResponseStatus");
1513+
}
1514+
1515+
/**
1516+
* Verify that when using the quarkus library with interfaceOnly=false (concrete stub class),
1517+
* the generated implementation method is also annotated with {@code @ResponseStatus(<code>)}.
1518+
*/
1519+
@Test
1520+
public void generateQuarkusConcreteClassAddsResponseStatusAnnotation() throws Exception {
1521+
final File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
1522+
output.deleteOnExit();
1523+
1524+
final OpenAPI openAPI = new OpenAPIParser()
1525+
.readLocation("src/test/resources/3_0/ping.yaml", null, new ParseOptions()).getOpenAPI();
1526+
1527+
codegen.setOutputDir(output.getAbsolutePath());
1528+
codegen.setLibrary(QUARKUS_LIBRARY);
1529+
codegen.additionalProperties().put(USE_JAKARTA_EE, true); //Required: @ResponseStatus is only emitted for Jakarta EE (Quarkus 3+)
1530+
codegen.additionalProperties().put(INTERFACE_ONLY, false); //Given the concrete class is generated
1531+
1532+
final ClientOptInput input = new ClientOptInput()
1533+
.openAPI(openAPI)
1534+
.config(codegen);
1535+
1536+
final DefaultGenerator generator = new DefaultGenerator();
1537+
final List<File> files = generator.opts(input).generate();
1538+
1539+
validateJavaSourceFiles(files);
1540+
1541+
//Then the generated class contains the ResponseStatus import and annotation with code 201
1542+
TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/PingApi.java");
1543+
assertFileContains(output.toPath().resolve("src/gen/java/org/openapitools/api/PingApi.java"),
1544+
"import org.jboss.resteasy.reactive.ResponseStatus;",
1545+
"@ResponseStatus(201)");
1546+
}
13451547
}

0 commit comments

Comments
 (0)