Skip to content

Commit 685d9b9

Browse files
authored
[Java][jersey3] Add error entity deserialization to ApiException (#23542)
* [Java][jersey3] Add error entity deserialization to ApiException - Add errorEntity field and getErrorEntity() method to ApiException - Add deserializeErrorEntity method to ApiClient - Pass errorTypes map from API methods to invokeAPI - Enables automatic deserialization of error response bodies - Fixes #4777 * Add unit tests for errorEntity deserialization feature - Add JavaJersey3ErrorEntityTest with 8 test cases - Tests verify template changes for errorEntity feature - 642 Java tests passed - no regressions - Fixes #4777 * Fix forbidden API check: specify UTF-8 charset The test was using String(byte[]) without specifying charset, which is flagged by forbiddenapis as using the default charset. * chore: regenerate Java jersey3 samples with errorEntity feature - Regenerate samples to verify templates work correctly - ApiException now contains errorEntity and getErrorEntity() - API methods include localVarErrorTypes for error deserialization - Fixes #4777 * fix: return null instead of error message on deserialization failure When deserializeErrorEntity fails, return null instead of a synthetic String message to maintain correct errorEntity semantics (null on failure). Fixes P2 issue from cubic-dev-ai review. * chore: regenerate jersey3-oneOf sample with errorEntity feature * test: add functional test for errorEntity deserialization - Add JavaJersey3ErrorEntityFunctionalTest - Verifies generated templates include errorEntity field and methods - Verifies deserializeErrorEntity returns null on failure (P2 fix) - Related to issue #4777 and PR #23542 * test: fix path issue in functional test - Correct the JERSEY3_TEMPLATE_DIR path - All 4 functional tests now pass * fix: address review comments from wing328 - Add docstring to deserializeErrorEntity method - Remove SmartBear Software copyright from test files * chore: regenerate jersey3 samples after adding docstring to deserializeErrorEntity * chore: regenerate samples without generation timestamp Regenerate jersey3 and jersey3-oneOf samples with hideGenerationTimestamp=true to fix P2 non-deterministic timestamp issues in @generated annotations. * fix: add missing dependencies for jersey3 samples Add jakarta.validation-api and commons-lang3 dependencies to fix compilation errors in Quadrilateral, SimpleQuadrilateral, and ComplexQuadrilateral models. * feat(jersey3): add error entity deserialization support - Add errorEntity field and getErrorEntity() method to ApiException - Add deserializeErrorEntity() method to ApiClient for error deserialization - Update API methods to pass errorTypes map for automatic error handling - Add unit tests for errorEntity feature - Regenerate jersey3 and jersey3-oneOf samples - Fix sample pom.xml to include required dependencies (validation, commons-lang3, http-signature) * feat(jersey3): add error entity deserialization support - Add errorEntity field and getErrorEntity() method to ApiException - Add deserializeErrorEntity() method to ApiClient for error deserialization - Update API methods to pass errorTypes map for automatic error handling - Add unit tests for errorEntity feature * feat(jersey3): add error entity deserialization support - Add errorEntity field and getErrorEntity() method to ApiException - Add deserializeErrorEntity() method to ApiClient for error deserialization - Update API methods to pass errorTypes map for automatic error handling - Add unit tests for errorEntity feature - Regenerate jersey3 and jersey3-oneOf samples - Fix sample pom.xml to include required dependencies (validation, commons-lang3, http-signature) * fix: address cubic-dev-ai review comments - Remove {{^-first}} to include ALL responses in errorType map (not just from 2nd response) - Add transient keyword to errorEntity field for serialization safety - Update test to expect transient keyword - Regenerate samples with fixes * fix: remove duplicate junit-version property from pom.xml Duplicate property removed to fix P2 issue from cubic-dev-ai review. * chore: regenerate jersey3 samples to fix compilation errors
1 parent 40f9887 commit 685d9b9

18 files changed

Lines changed: 686 additions & 57 deletions

File tree

modules/openapi-generator/src/main/resources/Java/libraries/jersey3/ApiClient.mustache

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import java.util.List;
5454
import java.util.Arrays;
5555
import java.util.ArrayList;
5656
import java.util.Date;
57+
import java.util.Locale;
5758
import java.util.stream.Collectors;
5859
import java.util.stream.Stream;
5960
{{#jsr310}}
@@ -1196,6 +1197,7 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} {
11961197
* @param authNames The authentications to apply
11971198
* @param returnType The return type into which to deserialize the response
11981199
* @param isBodyNullable True if the body is nullable
1200+
* @param errorTypes Mapping of error codes to types into which to deserialize the response
11991201
* @return The response body in type of string
12001202
* @throws ApiException API exception
12011203
*/
@@ -1212,7 +1214,9 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} {
12121214
String contentType,
12131215
String[] authNames,
12141216
GenericType<T> returnType,
1215-
boolean isBodyNullable)
1217+
boolean isBodyNullable,
1218+
Map<String, GenericType> errorTypes
1219+
)
12161220
throws ApiException {
12171221
12181222
String targetURL;
@@ -1223,7 +1227,7 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} {
12231227
if (index < 0 || index >= serverConfigurations.size()) {
12241228
throw new ArrayIndexOutOfBoundsException(
12251229
String.format(
1226-
java.util.Locale.ROOT,
1230+
Locale.ROOT,
12271231
"Invalid index %d when selecting the host settings. Must be less than %d",
12281232
index, serverConfigurations.size()));
12291233
}
@@ -1333,14 +1337,16 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} {
13331337
String respBody = null;
13341338
if (response.hasEntity()) {
13351339
try {
1340+
// call bufferEntity, so that a subsequent call to `readEntity` in `deserialize` doesn't fail
1341+
response.bufferEntity();
13361342
respBody = String.valueOf(response.readEntity(String.class));
13371343
message = respBody;
13381344
} catch (RuntimeException e) {
13391345
// e.printStackTrace();
13401346
}
13411347
}
13421348
throw new ApiException(
1343-
response.getStatus(), message, buildResponseHeaders(response), respBody);
1349+
response.getStatus(), message, buildResponseHeaders(response), respBody, deserializeErrorEntity(errorTypes, response));
13441350
}
13451351
} finally {
13461352
try {
@@ -1351,6 +1357,30 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} {
13511357
}
13521358
}
13531359
}
1360+
1361+
/**
1362+
* Deserialize the response body into an error entity based on HTTP status code.
1363+
* Looks up the error type from the errorTypes map using the response status code,
1364+
* or falls back to the "default" error type if no match is found.
1365+
*
1366+
* @param errorTypes Map of status code strings to GenericType for deserialization
1367+
* @param response The HTTP response
1368+
* @return The deserialized error entity, or null if not found or deserialization fails
1369+
*/
1370+
private Object deserializeErrorEntity(Map<String, GenericType> errorTypes, Response response) {
1371+
if (errorTypes == null) {
1372+
return null;
1373+
}
1374+
GenericType errorType = errorTypes.get(String.valueOf(response.getStatus()));
1375+
if (errorType == null) {
1376+
errorType = errorTypes.get("0"); // "0" is the "default" response
1377+
}
1378+
try {
1379+
return deserialize(response, errorType);
1380+
} catch (Exception e) {
1381+
return null;
1382+
}
1383+
}
13541384
13551385
protected Response sendRequest(String method, Invocation.Builder invocationBuilder, Entity<?> entity) {
13561386
Response response;
@@ -1377,7 +1407,7 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} {
13771407
*/
13781408
@Deprecated
13791409
public <T> ApiResponse<T> invokeAPI(String path, String method, List<Pair> queryParams, Object body, Map<String, String> headerParams, Map<String, String> cookieParams, Map<String, Object> formParams, String accept, String contentType, String[] authNames, GenericType<T> returnType, boolean isBodyNullable) throws ApiException {
1380-
return invokeAPI(null, path, method, queryParams, body, headerParams, cookieParams, formParams, accept, contentType, authNames, returnType, isBodyNullable);
1410+
return invokeAPI(null, path, method, queryParams, body, headerParams, cookieParams, formParams, accept, contentType, authNames, returnType, isBodyNullable, null/*TODO SME manage*/);
13811411
}
13821412
13831413
/**

modules/openapi-generator/src/main/resources/Java/libraries/jersey3/api.mustache

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,12 +195,18 @@ public class {{classname}} {
195195
{{#hasAuthMethods}}
196196
String[] localVarAuthNames = {{=% %=}}new String[] {%#authMethods%"%name%"%^-last%, %/-last%%/authMethods%};%={{ }}=%
197197
{{/hasAuthMethods}}
198+
final Map<String, GenericType> localVarErrorTypes = new HashMap<String, GenericType>();
199+
{{#responses}}
200+
{{#dataType}}
201+
localVarErrorTypes.put("{{code}}", new GenericType<{{{dataType}}}>() {});
202+
{{/dataType}}
203+
{{/responses}}
198204
{{#returnType}}
199205
GenericType<{{{returnType}}}> localVarReturnType = new GenericType<{{{returnType}}}>() {};
200206
{{/returnType}}
201207
return apiClient.invokeAPI("{{classname}}.{{operationId}}", {{#hasPathParams}}localVarPath{{/hasPathParams}}{{^hasPathParams}}"{{{path}}}"{{/hasPathParams}}, "{{httpMethod}}", {{#queryParams}}{{#-first}}localVarQueryParams{{/-first}}{{/queryParams}}{{^queryParams}}new ArrayList<>(){{/queryParams}}, {{#bodyParam}}{{paramName}}{{/bodyParam}}{{^bodyParam}}null{{/bodyParam}},
202208
{{#headerParams}}{{#-first}}localVarHeaderParams{{/-first}}{{/headerParams}}{{^headerParams}}new LinkedHashMap<>(){{/headerParams}}, {{#cookieParams}}{{#-first}}localVarCookieParams{{/-first}}{{/cookieParams}}{{^cookieParams}}new LinkedHashMap<>(){{/cookieParams}}, {{#formParams}}{{#-first}}localVarFormParams{{/-first}}{{/formParams}}{{^formParams}}new LinkedHashMap<>(){{/formParams}}, localVarAccept, localVarContentType,
203-
{{#hasAuthMethods}}localVarAuthNames{{/hasAuthMethods}}{{^hasAuthMethods}}null{{/hasAuthMethods}}, {{#returnType}}localVarReturnType{{/returnType}}{{^returnType}}null{{/returnType}}, {{#bodyParam}}{{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/bodyParam}}{{^bodyParam}}false{{/bodyParam}});
209+
{{#hasAuthMethods}}localVarAuthNames{{/hasAuthMethods}}{{^hasAuthMethods}}null{{/hasAuthMethods}}, {{#returnType}}localVarReturnType{{/returnType}}{{^returnType}}null{{/returnType}}, {{#bodyParam}}{{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/bodyParam}}{{^bodyParam}}false{{/bodyParam}}, localVarErrorTypes);
204210
}
205211
{{#vendorExtensions.x-group-parameters}}
206212

modules/openapi-generator/src/main/resources/Java/libraries/jersey3/apiException.mustache

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public class ApiException extends{{#useRuntimeException}} RuntimeException {{/us
2020
private int code = 0;
2121
private Map<String, List<String>> responseHeaders = null;
2222
private String responseBody = null;
23+
private transient Object errorEntity = null;
2324
2425
public ApiException() {}
2526

@@ -73,6 +74,11 @@ public class ApiException extends{{#useRuntimeException}} RuntimeException {{/us
7374
this.responseBody = responseBody;
7475
}
7576

77+
public ApiException(int code, String message, Map<String, List<String>> responseHeaders, String responseBody, Object errorEntity) {
78+
this(code, message, responseHeaders, responseBody);
79+
this.errorEntity = errorEntity;
80+
}
81+
7682
/**
7783
* Get the HTTP status code.
7884
*
@@ -99,4 +105,13 @@ public class ApiException extends{{#useRuntimeException}} RuntimeException {{/us
99105
public String getResponseBody() {
100106
return responseBody;
101107
}
108+
109+
/**
110+
* Get the deserialized error entity (or null if this error doesn't have a model associated).
111+
*
112+
* @return Deserialized error entity
113+
*/
114+
public Object getErrorEntity() {
115+
return errorEntity;
116+
}
102117
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.openapitools.codegen.java.jersey3;
18+
19+
import org.testng.annotations.Test;
20+
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
24+
import static org.testng.Assert.*;
25+
26+
/**
27+
* Functional test for errorEntity deserialization feature.
28+
*
29+
* This test verifies that the generated code includes the errorEntity
30+
* field and getErrorEntity() method by examining the generated templates.
31+
*
32+
* Full integration tests would require:
33+
* 1. A running mock HTTP server
34+
* 2. Generated client code compiled and executed
35+
* 3. Actual API calls to verify runtime behavior
36+
*
37+
* The client's original project (BudgetApiTest) provides this type of
38+
* functional testing. This test verifies the template structure is correct.
39+
*/
40+
public class JavaJersey3ErrorEntityFunctionalTest {
41+
42+
private static final String JERSEY3_TEMPLATE_DIR =
43+
"src/main/resources/Java/libraries/jersey3/";
44+
45+
/**
46+
* Verify generated code includes errorEntity field in ApiException
47+
*/
48+
@Test
49+
public void testGeneratedApiExceptionHasErrorEntity() throws Exception {
50+
String template = readTemplate("apiException.mustache");
51+
assertNotNull(template);
52+
53+
// Verify errorEntity field exists (transient for serialization safety)
54+
assertTrue(template.contains("private transient Object errorEntity = null"),
55+
"Generated ApiException should have errorEntity field");
56+
57+
// Verify getErrorEntity() method exists
58+
assertTrue(template.contains("public Object getErrorEntity()"),
59+
"Generated ApiException should have getErrorEntity() method");
60+
61+
// Verify constructor with errorEntity parameter
62+
assertTrue(template.contains("Object errorEntity"),
63+
"Generated ApiException should accept errorEntity in constructor");
64+
}
65+
66+
/**
67+
* Verify generated code includes deserializeErrorEntity method
68+
*/
69+
@Test
70+
public void testGeneratedApiClientHasDeserializeErrorEntity() throws Exception {
71+
String template = readTemplate("ApiClient.mustache");
72+
assertNotNull(template);
73+
74+
// Verify deserializeErrorEntity method exists
75+
assertTrue(template.contains("deserializeErrorEntity"),
76+
"Generated ApiClient should have deserializeErrorEntity method");
77+
78+
// Verify errorTypes parameter handling
79+
assertTrue(template.contains("Map<String, GenericType> errorTypes"),
80+
"Generated ApiClient should handle errorTypes parameter");
81+
}
82+
83+
/**
84+
* Verify generated API methods build error types map
85+
*/
86+
@Test
87+
public void testGeneratedApiMethodsBuildErrorTypesMap() throws Exception {
88+
String template = readTemplate("api.mustache");
89+
assertNotNull(template);
90+
91+
// Verify localVarErrorTypes is built
92+
assertTrue(template.contains("localVarErrorTypes"),
93+
"Generated API methods should build localVarErrorTypes");
94+
95+
// Verify error types are put into the map
96+
assertTrue(template.contains("localVarErrorTypes.put"),
97+
"Generated API methods should put error types into map");
98+
}
99+
100+
/**
101+
* Verify null is returned when deserialization fails
102+
*/
103+
@Test
104+
public void testDeserializationReturnsNullOnFailure() throws Exception {
105+
String template = readTemplate("ApiClient.mustache");
106+
assertNotNull(template);
107+
108+
// Verify that on exception, null is returned (not error message)
109+
// This is the fix we applied for the P2 issue
110+
assertFalse(template.contains("String.format(\"Failed deserializing"),
111+
"deserializeErrorEntity should return null, not error message string");
112+
}
113+
114+
/**
115+
* Helper method to read template files
116+
*/
117+
private String readTemplate(String templateName) throws Exception {
118+
java.nio.file.Path templatePath = java.nio.file.Paths.get(JERSEY3_TEMPLATE_DIR + templateName);
119+
if (java.nio.file.Files.exists(templatePath)) {
120+
return java.nio.file.Files.readString(templatePath);
121+
}
122+
return null;
123+
}
124+
}

0 commit comments

Comments
 (0)