Skip to content

Commit 1eea89d

Browse files
committed
feat: deserialize typed error responses via ApiException.getErrorEntity()
1 parent 7a4422d commit 1eea89d

10 files changed

Lines changed: 178 additions & 15 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"java-sdk": minor
3+
---
4+
5+
**sdk**: Bump `openapi-generator` to 7.24.0 and add `ApiException.getErrorEntity()`, which automatically deserializes error response bodies into their typed model (e.g. `ErrorResponse`) when available.

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,19 @@ public class FingerprintApiExample {
187187
}
188188
```
189189

190+
## Handling errors
191+
192+
`ApiException` exposes the raw response body via `getResponseBody()`. When it matches a known model, it's also available pre-deserialized via `getErrorEntity()`:
193+
194+
```java
195+
} catch (ApiException e) {
196+
ErrorResponse errorResponse = (ErrorResponse) e.getErrorEntity();
197+
if (errorResponse != null) {
198+
System.err.println(errorResponse.getError().getMessage());
199+
}
200+
}
201+
```
202+
190203
## Sealed results
191204

192205
This SDK provides utility methods for decoding [sealed results](https://dev.fingerprint.com/docs/sealed-client-results).

sdk/src/main/java/com/fingerprint/v4/api/FingerprintApi.java

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.fingerprint.v4.model.BotInfoCategory;
44
import com.fingerprint.v4.model.BotInfoConfidence;
55
import com.fingerprint.v4.model.BotInfoIdentity;
6+
import com.fingerprint.v4.model.ErrorResponse;
67
import com.fingerprint.v4.model.Event;
78
import com.fingerprint.v4.model.EventSearch;
89
import com.fingerprint.v4.model.EventUpdate;
@@ -127,6 +128,11 @@ public ApiResponse<Void> deleteVisitorDataWithHttpInfo(String visitorId) throws
127128
final String localVarAccept = apiClient.selectHeaderAccept("application/json");
128129
final String localVarContentType = apiClient.selectHeaderContentType();
129130
String[] localVarAuthNames = new String[] {"bearerAuth"};
131+
final Map<String, GenericType<?>> localVarErrorTypes = new HashMap<String, GenericType<?>>();
132+
localVarErrorTypes.put("400", new GenericType<ErrorResponse>() {});
133+
localVarErrorTypes.put("403", new GenericType<ErrorResponse>() {});
134+
localVarErrorTypes.put("404", new GenericType<ErrorResponse>() {});
135+
localVarErrorTypes.put("429", new GenericType<ErrorResponse>() {});
130136

131137
return apiClient.invokeAPI(
132138
"FingerprintApi.deleteVisitorData",
@@ -141,7 +147,8 @@ public ApiResponse<Void> deleteVisitorDataWithHttpInfo(String visitorId) throws
141147
localVarContentType,
142148
localVarAuthNames,
143149
null,
144-
false);
150+
false,
151+
localVarErrorTypes);
145152
}
146153

147154
public static class GetEventOptionalParams {
@@ -255,6 +262,13 @@ public ApiResponse<Event> getEventWithHttpInfo(
255262
final String localVarAccept = apiClient.selectHeaderAccept("application/json");
256263
final String localVarContentType = apiClient.selectHeaderContentType();
257264
String[] localVarAuthNames = new String[] {"bearerAuth"};
265+
final Map<String, GenericType<?>> localVarErrorTypes = new HashMap<String, GenericType<?>>();
266+
localVarErrorTypes.put("200", new GenericType<Event>() {});
267+
localVarErrorTypes.put("400", new GenericType<ErrorResponse>() {});
268+
localVarErrorTypes.put("403", new GenericType<ErrorResponse>() {});
269+
localVarErrorTypes.put("404", new GenericType<ErrorResponse>() {});
270+
localVarErrorTypes.put("429", new GenericType<ErrorResponse>() {});
271+
localVarErrorTypes.put("500", new GenericType<ErrorResponse>() {});
258272

259273
GenericType<Event> localVarReturnType = new GenericType<Event>() {};
260274

@@ -271,7 +285,8 @@ public ApiResponse<Event> getEventWithHttpInfo(
271285
localVarContentType,
272286
localVarAuthNames,
273287
localVarReturnType,
274-
false);
288+
false,
289+
localVarErrorTypes);
275290
}
276291

277292
public static class SearchEventsOptionalParams {
@@ -1347,6 +1362,12 @@ public ApiResponse<EventSearch> searchEventsWithHttpInfo(
13471362
final String localVarAccept = apiClient.selectHeaderAccept("application/json");
13481363
final String localVarContentType = apiClient.selectHeaderContentType();
13491364
String[] localVarAuthNames = new String[] {"bearerAuth"};
1365+
final Map<String, GenericType<?>> localVarErrorTypes = new HashMap<String, GenericType<?>>();
1366+
localVarErrorTypes.put("200", new GenericType<EventSearch>() {});
1367+
localVarErrorTypes.put("400", new GenericType<ErrorResponse>() {});
1368+
localVarErrorTypes.put("403", new GenericType<ErrorResponse>() {});
1369+
localVarErrorTypes.put("404", new GenericType<ErrorResponse>() {});
1370+
localVarErrorTypes.put("500", new GenericType<ErrorResponse>() {});
13501371

13511372
GenericType<EventSearch> localVarReturnType = new GenericType<EventSearch>() {};
13521373

@@ -1363,7 +1384,8 @@ public ApiResponse<EventSearch> searchEventsWithHttpInfo(
13631384
localVarContentType,
13641385
localVarAuthNames,
13651386
localVarReturnType,
1366-
false);
1387+
false,
1388+
localVarErrorTypes);
13671389
}
13681390

13691391
/**
@@ -1435,6 +1457,11 @@ public ApiResponse<Void> updateEventWithHttpInfo(String eventId, EventUpdate eve
14351457
final String localVarAccept = apiClient.selectHeaderAccept("application/json");
14361458
final String localVarContentType = apiClient.selectHeaderContentType("application/json");
14371459
String[] localVarAuthNames = new String[] {"bearerAuth"};
1460+
final Map<String, GenericType<?>> localVarErrorTypes = new HashMap<String, GenericType<?>>();
1461+
localVarErrorTypes.put("400", new GenericType<ErrorResponse>() {});
1462+
localVarErrorTypes.put("403", new GenericType<ErrorResponse>() {});
1463+
localVarErrorTypes.put("404", new GenericType<ErrorResponse>() {});
1464+
localVarErrorTypes.put("409", new GenericType<ErrorResponse>() {});
14381465

14391466
return apiClient.invokeAPI(
14401467
"FingerprintApi.updateEvent",
@@ -1449,6 +1476,7 @@ public ApiResponse<Void> updateEventWithHttpInfo(String eventId, EventUpdate eve
14491476
localVarContentType,
14501477
localVarAuthNames,
14511478
null,
1452-
false);
1479+
false,
1480+
localVarErrorTypes);
14531481
}
14541482
}

sdk/src/main/java/com/fingerprint/v4/sdk/ApiClient.java

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,7 @@ public File prepareDownloadFile(Response response) throws IOException {
999999
* @param authNames The authentications to apply
10001000
* @param returnType The return type into which to deserialize the response
10011001
* @param isBodyNullable True if the body is nullable
1002+
* @param errorTypes Mapping of error codes to types into which to deserialize the response
10021003
* @return The response body in type of string
10031004
* @throws ApiException API exception
10041005
*/
@@ -1015,7 +1016,8 @@ public <T> ApiResponse<T> invokeAPI(
10151016
String contentType,
10161017
String[] authNames,
10171018
GenericType<T> returnType,
1018-
boolean isBodyNullable)
1019+
boolean isBodyNullable,
1020+
Map<String, GenericType<?>> errorTypes)
10191021
throws ApiException {
10201022

10211023
String targetURL;
@@ -1109,14 +1111,21 @@ public <T> ApiResponse<T> invokeAPI(
11091111
String respBody = null;
11101112
if (response.hasEntity()) {
11111113
try {
1114+
// call bufferEntity, so that a subsequent call to `readEntity` in `deserialize` doesn't
1115+
// fail
1116+
response.bufferEntity();
11121117
respBody = String.valueOf(response.readEntity(String.class));
11131118
message = respBody;
11141119
} catch (RuntimeException e) {
11151120
// e.printStackTrace();
11161121
}
11171122
}
11181123
throw new ApiException(
1119-
response.getStatus(), message, buildResponseHeaders(response), respBody);
1124+
response.getStatus(),
1125+
message,
1126+
buildResponseHeaders(response),
1127+
respBody,
1128+
deserializeErrorEntity(errorTypes, response));
11201129
}
11211130
} finally {
11221131
try {
@@ -1128,6 +1137,30 @@ public <T> ApiResponse<T> invokeAPI(
11281137
}
11291138
}
11301139

1140+
/**
1141+
* Deserialize the response body into an error entity based on HTTP status code.
1142+
* Looks up the error type from the errorTypes map using the response status code,
1143+
* or falls back to the "default" error type if no match is found.
1144+
*
1145+
* @param errorTypes Map of status code strings to GenericType for deserialization
1146+
* @param response The HTTP response
1147+
* @return The deserialized error entity, or null if not found or deserialization fails
1148+
*/
1149+
private Object deserializeErrorEntity(Map<String, GenericType<?>> errorTypes, Response response) {
1150+
if (errorTypes == null) {
1151+
return null;
1152+
}
1153+
GenericType<?> errorType = errorTypes.get(String.valueOf(response.getStatus()));
1154+
if (errorType == null) {
1155+
errorType = errorTypes.get("0"); // "0" is the "default" response
1156+
}
1157+
try {
1158+
return deserialize(response, errorType);
1159+
} catch (Exception e) {
1160+
return null;
1161+
}
1162+
}
1163+
11311164
protected Response sendRequest(
11321165
String method, Invocation.Builder invocationBuilder, Entity<?> entity) {
11331166
Response response;
@@ -1180,7 +1213,8 @@ public <T> ApiResponse<T> invokeAPI(
11801213
contentType,
11811214
authNames,
11821215
returnType,
1183-
isBodyNullable);
1216+
isBodyNullable,
1217+
null);
11841218
}
11851219

11861220
/**

sdk/src/main/java/com/fingerprint/v4/sdk/ApiException.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public class ApiException extends Exception {
2727
private int code = 0;
2828
private transient Map<String, List<String>> responseHeaders = null;
2929
private String responseBody = null;
30+
private transient Object errorEntity = null;
3031

3132
public ApiException() {}
3233

@@ -76,6 +77,16 @@ public ApiException(
7677
this.responseBody = responseBody;
7778
}
7879

80+
public ApiException(
81+
int code,
82+
String message,
83+
Map<String, List<String>> responseHeaders,
84+
String responseBody,
85+
Object errorEntity) {
86+
this(code, message, responseHeaders, responseBody);
87+
this.errorEntity = errorEntity;
88+
}
89+
7990
/**
8091
* Get the HTTP status code.
8192
*
@@ -102,4 +113,13 @@ public Map<String, List<String>> getResponseHeaders() {
102113
public String getResponseBody() {
103114
return responseBody;
104115
}
116+
117+
/**
118+
* Get the deserialized error entity (or null if this error doesn't have a model associated).
119+
*
120+
* @return Deserialized error entity
121+
*/
122+
public Object getErrorEntity() {
123+
return errorEntity;
124+
}
105125
}

sdk/src/test/java/com/fingerprint/v4/api/FingerprintApiTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,8 @@ private <T> void addMock(String operation, String path, ApiAnswerFunction<T> ans
140140
any(), // contentType
141141
any(), // authNames
142142
any(), // returnType
143-
eq(false) // isBodyNullable
143+
eq(false), // isBodyNullable
144+
any() // errorTypes
144145
);
145146
}
146147

template/README.mustache

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,19 @@ public class FingerprintApiExample {
187187
}
188188
```
189189
190+
## Handling errors
191+
192+
`ApiException` exposes the raw response body via `getResponseBody()`. When it matches a known model, it's also available pre-deserialized via `getErrorEntity()`:
193+
194+
```java
195+
} catch (ApiException e) {
196+
ErrorResponse errorResponse = (ErrorResponse) e.getErrorEntity();
197+
if (errorResponse != null) {
198+
System.err.println(errorResponse.getError().getMessage());
199+
}
200+
}
201+
```
202+
190203
## Sealed results
191204

192205
This SDK provides utility methods for decoding [sealed results](https://dev.fingerprint.com/docs/sealed-client-results).

template/libraries/jersey3/ApiClient.mustache

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,7 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} {
11991199
* @param authNames The authentications to apply
12001200
* @param returnType The return type into which to deserialize the response
12011201
* @param isBodyNullable True if the body is nullable
1202+
* @param errorTypes Mapping of error codes to types into which to deserialize the response
12021203
* @return The response body in type of string
12031204
* @throws ApiException API exception
12041205
*/
@@ -1215,7 +1216,8 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} {
12151216
String contentType,
12161217
String[] authNames,
12171218
GenericType<T> returnType,
1218-
boolean isBodyNullable)
1219+
boolean isBodyNullable,
1220+
Map<String, GenericType<?>> errorTypes)
12191221
throws ApiException {
12201222
12211223
String targetURL;
@@ -1301,11 +1303,9 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} {
13011303
try {
13021304
response = sendRequest(method, invocationBuilder, entity);
13031305
1304-
final int statusCode = response.getStatusInfo().getStatusCode();
1305-
13061306
{{#hasOAuthMethods}}
13071307
// If OAuth is used and a status 401 is received, renew the access token and retry the request
1308-
if (authNames != null && statusCode == Status.UNAUTHORIZED.getStatusCode()) {
1308+
if (authNames != null && response.getStatusInfo().getStatusCode() == Status.UNAUTHORIZED.getStatusCode()) {
13091309
for (String authName : authNames) {
13101310
Authentication authentication = authentications.get(authName);
13111311
if (authentication instanceof OAuth) {
@@ -1321,6 +1321,8 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} {
13211321
}
13221322

13231323
{{/hasOAuthMethods}}
1324+
final int statusCode = response.getStatusInfo().getStatusCode();
1325+
13241326
Map<String, List<String>> responseHeaders = buildResponseHeaders(response);
13251327

13261328
if (statusCode == Status.NO_CONTENT.getStatusCode()) {
@@ -1336,14 +1338,16 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} {
13361338
String respBody = null;
13371339
if (response.hasEntity()) {
13381340
try {
1341+
// call bufferEntity, so that a subsequent call to `readEntity` in `deserialize` doesn't fail
1342+
response.bufferEntity();
13391343
respBody = String.valueOf(response.readEntity(String.class));
13401344
message = respBody;
13411345
} catch (RuntimeException e) {
13421346
// e.printStackTrace();
13431347
}
13441348
}
13451349
throw new ApiException(
1346-
response.getStatus(), message, buildResponseHeaders(response), respBody);
1350+
response.getStatus(), message, buildResponseHeaders(response), respBody, deserializeErrorEntity(errorTypes, response));
13471351
}
13481352
} finally {
13491353
try {
@@ -1355,6 +1359,30 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} {
13551359
}
13561360
}
13571361
1362+
/**
1363+
* Deserialize the response body into an error entity based on HTTP status code.
1364+
* Looks up the error type from the errorTypes map using the response status code,
1365+
* or falls back to the "default" error type if no match is found.
1366+
*
1367+
* @param errorTypes Map of status code strings to GenericType for deserialization
1368+
* @param response The HTTP response
1369+
* @return The deserialized error entity, or null if not found or deserialization fails
1370+
*/
1371+
private Object deserializeErrorEntity(Map<String, GenericType<?>> errorTypes, Response response) {
1372+
if (errorTypes == null) {
1373+
return null;
1374+
}
1375+
GenericType<?> errorType = errorTypes.get(String.valueOf(response.getStatus()));
1376+
if (errorType == null) {
1377+
errorType = errorTypes.get("0"); // "0" is the "default" response
1378+
}
1379+
try {
1380+
return deserialize(response, errorType);
1381+
} catch (Exception e) {
1382+
return null;
1383+
}
1384+
}
1385+
13581386
protected Response sendRequest(String method, Invocation.Builder invocationBuilder, Entity<?> entity) {
13591387
Response response;
13601388
if ("POST".equals(method)) {
@@ -1380,7 +1408,7 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} {
13801408
*/
13811409
@Deprecated
13821410
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 {
1383-
return invokeAPI(null, path, method, queryParams, body, headerParams, cookieParams, formParams, accept, contentType, authNames, returnType, isBodyNullable);
1411+
return invokeAPI(null, path, method, queryParams, body, headerParams, cookieParams, formParams, accept, contentType, authNames, returnType, isBodyNullable, null);
13841412
}
13851413
13861414
/**

template/libraries/jersey3/api.mustache

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,14 +275,20 @@ public class {{classname}} {
275275
final String localVarAccept = apiClient.selectHeaderAccept({{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}});
276276
final String localVarContentType = apiClient.selectHeaderContentType({{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}});
277277
String[] localVarAuthNames = new String[] { {{#authMethods}}"{{name}}"{{^-last}}, {{/-last}}{{/authMethods}} };
278+
final Map<String, GenericType<?>> localVarErrorTypes = new HashMap<String, GenericType<?>>();
279+
{{#responses}}
280+
{{#dataType}}
281+
localVarErrorTypes.put("{{code}}", new GenericType<{{{dataType}}}>() {});
282+
{{/dataType}}
283+
{{/responses}}
278284

279285
{{#returnType}}
280286
GenericType<{{{returnType}}}> localVarReturnType = new GenericType<{{{returnType}}}>() {};
281287

282288
{{/returnType}}
283289
return apiClient.invokeAPI("{{classname}}.{{operationId}}", localVarPath, "{{httpMethod}}", localVarQueryParams, localVarPostBody,
284290
localVarHeaderParams, localVarCookieParams, localVarFormParams, localVarAccept, localVarContentType,
285-
localVarAuthNames, {{#returnType}}localVarReturnType{{/returnType}}{{^returnType}}null{{/returnType}}, {{#bodyParam}}{{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/bodyParam}}{{^bodyParam}}false{{/bodyParam}});
291+
localVarAuthNames, {{#returnType}}localVarReturnType{{/returnType}}{{^returnType}}null{{/returnType}}, {{#bodyParam}}{{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/bodyParam}}{{^bodyParam}}false{{/bodyParam}}, localVarErrorTypes);
286292
}
287293
{{/operation}}
288294
}

0 commit comments

Comments
 (0)