Skip to content

Commit dd96c5b

Browse files
authored
Handle edge cases when deserializing error JSON (#242)
1 parent 29e8993 commit dd96c5b

File tree

3 files changed

+144
-43
lines changed

3 files changed

+144
-43
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG
22

3+
## v6.4.0 Next Release
4+
5+
- Handle edge cases when deseralizing error JSON, close Github issue [239](https://github.com/EasyPost/easypost-java/issues/239).
6+
37
## v6.3.0 (2023-02-21)
48

59
- Adds beta `retrieveStatelessRates` function to get stateless rates

src/main/java/com/easypost/model/ErrorDeserializer.java

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,32 @@
1212

1313
import java.lang.reflect.Type;
1414
import java.util.ArrayList;
15+
import java.util.Map.Entry;
1516

1617
public final class ErrorDeserializer implements JsonDeserializer<Error> {
18+
/**
19+
* Recursively traverse an error JSON element and its sub-element(s), and extracts all
20+
* error string values found into the specified string list.
21+
*
22+
* @param element the JSON element to traverse
23+
* @param messagesList the list of strings to append found values to
24+
*/
25+
private void traverseJsonElement(JsonElement element, ArrayList<String> messagesList) {
26+
if (element.isJsonPrimitive()) {
27+
messagesList.add(element.getAsString());
28+
} else if (element.isJsonArray()) {
29+
JsonArray array = element.getAsJsonArray();
30+
for (JsonElement arrayElement : array) {
31+
traverseJsonElement(arrayElement, messagesList);
32+
}
33+
} else if (element.isJsonObject()) {
34+
JsonObject object = element.getAsJsonObject();
35+
for (Entry<String, JsonElement> entry : object.entrySet()) {
36+
traverseJsonElement(entry.getValue(), messagesList);
37+
}
38+
}
39+
}
40+
1741
/**
1842
* Deserialize an Error from a JSON object.
1943
*
@@ -25,7 +49,7 @@ public final class ErrorDeserializer implements JsonDeserializer<Error> {
2549
*/
2650
@Override
2751
public Error deserialize(final JsonElement json, final Type typeOfT,
28-
final JsonDeserializationContext context) throws JsonParseException{
52+
final JsonDeserializationContext context) throws JsonParseException {
2953
JsonObject jo = json.getAsJsonObject();
3054
JsonElement results = jo.get("error");
3155
Gson gson = new Gson();
@@ -36,18 +60,18 @@ public Error deserialize(final JsonElement json, final Type typeOfT,
3660
error.setCode("NO RESPONSE CODE");
3761
return error;
3862
}
39-
40-
JsonElement errorMessage = results.getAsJsonObject().get("message");
41-
if (errorMessage.isJsonArray()) {
42-
JsonArray jsonArray = errorMessage.getAsJsonArray();
43-
ArrayList<String> messages = new ArrayList<>();
44-
45-
for (int i = 0; i < jsonArray.size(); i++) {
46-
messages.add(jsonArray.get(i).getAsString());
47-
}
4863

64+
try {
65+
ArrayList<String> messages = new ArrayList<>();
66+
JsonElement errorMessageJson = results.getAsJsonObject().get("message");
67+
traverseJsonElement(errorMessageJson, messages);
4968
JsonPrimitive value = new JsonPrimitive(String.join(", ", messages));
5069
results.getAsJsonObject().add("message", value);
70+
} catch (Exception e) {
71+
Error error = new Error();
72+
error.setMessage("Error deserializing JSON response");
73+
error.setCode("ERROR_DESERIALIZATION_ERROR");
74+
return error;
5175
}
5276

5377
return gson.fromJson(results, Error.class);

src/test/java/com/easypost/ErrorTest.java

Lines changed: 106 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ public final class ErrorTest extends Requestor {
3232

3333
/**
3434
* Set up the testing environment for this file.
35+
*
3536
* @throws MissingParameterError
3637
*
37-
* @throws EasyPostException when the request fails.
38+
* @throws EasyPostException when the request fails.
3839
*/
3940
@BeforeAll
4041
public static void setup() throws MissingParameterError {
@@ -49,7 +50,7 @@ public static void setup() throws MissingParameterError {
4950
@Test
5051
public void testError() throws EasyPostException {
5152
vcr.setUpTest("error");
52-
53+
5354
APIException exception = assertThrows(InvalidRequestError.class, () -> vcr.client.shipment.create(null));
5455

5556
assertEquals(422, exception.getStatusCode());
@@ -67,31 +68,31 @@ public void testError() throws EasyPostException {
6768
@Test
6869
public void testKnownApiException() throws EasyPostException {
6970
HashMap<Integer, Class<?>> apiErrorsMap = new HashMap<Integer, Class<?>>();
70-
apiErrorsMap.put(300, RedirectError.class);
71-
apiErrorsMap.put(301, RedirectError.class);
72-
apiErrorsMap.put(302, RedirectError.class);
73-
apiErrorsMap.put(303, RedirectError.class);
74-
apiErrorsMap.put(304, RedirectError.class);
75-
apiErrorsMap.put(305, RedirectError.class);
76-
apiErrorsMap.put(306, RedirectError.class);
77-
apiErrorsMap.put(307, RedirectError.class);
78-
apiErrorsMap.put(308, RedirectError.class);
79-
apiErrorsMap.put(401, UnauthorizedError.class);
80-
apiErrorsMap.put(402, PaymentError.class);
81-
apiErrorsMap.put(403, ForbiddenError.class);
82-
apiErrorsMap.put(404, NotFoundError.class);
83-
apiErrorsMap.put(405, MethodNotAllowedError.class);
84-
apiErrorsMap.put(408, TimeoutError.class);
85-
apiErrorsMap.put(422, InvalidRequestError.class);
86-
apiErrorsMap.put(429, RateLimitError.class);
87-
apiErrorsMap.put(444, UnknownApiError.class);
88-
apiErrorsMap.put(500, InternalServerError.class);
89-
apiErrorsMap.put(503, ServiceUnavailableError.class);
90-
apiErrorsMap.put(504, GatewayTimeoutError.class);
91-
92-
for (Map.Entry<Integer, Class<?>> entry: apiErrorsMap.entrySet()) {
71+
apiErrorsMap.put(300, RedirectError.class);
72+
apiErrorsMap.put(301, RedirectError.class);
73+
apiErrorsMap.put(302, RedirectError.class);
74+
apiErrorsMap.put(303, RedirectError.class);
75+
apiErrorsMap.put(304, RedirectError.class);
76+
apiErrorsMap.put(305, RedirectError.class);
77+
apiErrorsMap.put(306, RedirectError.class);
78+
apiErrorsMap.put(307, RedirectError.class);
79+
apiErrorsMap.put(308, RedirectError.class);
80+
apiErrorsMap.put(401, UnauthorizedError.class);
81+
apiErrorsMap.put(402, PaymentError.class);
82+
apiErrorsMap.put(403, ForbiddenError.class);
83+
apiErrorsMap.put(404, NotFoundError.class);
84+
apiErrorsMap.put(405, MethodNotAllowedError.class);
85+
apiErrorsMap.put(408, TimeoutError.class);
86+
apiErrorsMap.put(422, InvalidRequestError.class);
87+
apiErrorsMap.put(429, RateLimitError.class);
88+
apiErrorsMap.put(444, UnknownApiError.class);
89+
apiErrorsMap.put(500, InternalServerError.class);
90+
apiErrorsMap.put(503, ServiceUnavailableError.class);
91+
apiErrorsMap.put(504, GatewayTimeoutError.class);
92+
93+
for (Map.Entry<Integer, Class<?>> entry : apiErrorsMap.entrySet()) {
9394
APIException exception = assertThrows(APIException.class,
94-
() -> handleAPIError("{}", entry.getKey()));
95+
() -> handleAPIError("{}", entry.getKey()));
9596

9697
assertEquals(Constants.ErrorMessages.API_DID_NOT_RETURN_ERROR_DETAILS, exception.getMessage());
9798
assertEquals("NO RESPONSE CODE", exception.getCode());
@@ -107,10 +108,15 @@ public void testKnownApiException() throws EasyPostException {
107108
*/
108109
@Test
109110
public void testExceptionErrorMessageParsing() throws EasyPostException {
110-
String errorMessageStringJson =
111-
"{\"error\": {\"code\": \"ERROR_CODE\", \"message\": \"ERROR_MESSAGE_1\", \"errors\": []}}";
111+
String errorMessageStringJson = "{\n" +
112+
" \"error\": {\n" +
113+
" \"code\": \"ERROR_CODE\",\n" +
114+
" \"message\": \"ERROR_MESSAGE_1\",\n" +
115+
" \"errors\": []\n" +
116+
" }\n" +
117+
"}";
112118
EasyPostException exception = assertThrows(EasyPostException.class,
113-
() -> handleAPIError(errorMessageStringJson, 400));
119+
() -> handleAPIError(errorMessageStringJson, 400));
114120

115121
assertEquals("ERROR_MESSAGE_1", exception.getMessage());
116122
}
@@ -122,12 +128,79 @@ public void testExceptionErrorMessageParsing() throws EasyPostException {
122128
*/
123129
@Test
124130
public void testExceptionErrorArrayParsing() throws EasyPostException {
125-
String errorMessageArrayJson = "{\"error\": {\"code\": \"ERROR_CODE\", \"message\":" +
126-
"[\"ERROR_MESSAGE_1\", \"ERROR_MESSAGE_2\"], \"errors\": []}}";
127-
131+
String errorMessageArrayJson = "{\n" +
132+
" \"error\": {\n" +
133+
" \"code\": \"ERROR_CODE\",\n" +
134+
" \"message\": [\n" +
135+
" \"ERROR_MESSAGE_1\",\n" +
136+
" \"ERROR_MESSAGE_2\"\n" +
137+
" ],\n" +
138+
" \"errors\": []\n" +
139+
" }\n" +
140+
"}";
128141
EasyPostException exception = assertThrows(EasyPostException.class,
129-
() -> handleAPIError(errorMessageArrayJson, 400));
142+
() -> handleAPIError(errorMessageArrayJson, 400));
130143

131144
assertEquals("ERROR_MESSAGE_1, ERROR_MESSAGE_2", exception.getMessage());
132145
}
146+
147+
/**
148+
* Test parsing error message that is an object.
149+
*
150+
* @throws EasyPostException
151+
*/
152+
@Test
153+
public void testExceptionErrorObjectParsing() throws EasyPostException {
154+
String errorMessageObjectJson = "{\n" +
155+
" \"error\": {\n" +
156+
" \"code\": \"UNPROCESSABLE_ENTITY\",\n" +
157+
" \"message\": {\n" +
158+
" \"errors\": [\n" +
159+
" \"bad error.\",\n" +
160+
" \"second bad error.\"\n" +
161+
" ]\n" +
162+
" },\n" +
163+
" \"errors\": []\n" +
164+
" }\n" +
165+
"}";
166+
167+
EasyPostException exception = assertThrows(EasyPostException.class,
168+
() -> handleAPIError(errorMessageObjectJson, 400));
169+
170+
assertEquals("bad error., second bad error.", exception.getMessage());
171+
}
172+
173+
/**
174+
* Test parsing error message that has really bad format.
175+
*
176+
* @throws EasyPostException
177+
*/
178+
@Test
179+
public void testExceptionErrorEdgeCaseParsing() throws EasyPostException {
180+
String json = "{\n" +
181+
" \"error\": {\n" +
182+
" \"code\": \"UNPROCESSABLE_ENTITY\",\n" +
183+
" \"message\": {\n" +
184+
" \"errors\": [\n" +
185+
" \"Bad format 1\",\n" +
186+
" \"Bad format 2\"\n" +
187+
" ],\n" +
188+
" \"bad_data\": [\n" +
189+
" {\n" +
190+
" \"first_message\": \"Bad format 3\",\n" +
191+
" \"second_message\": \"Bad format 4\",\n" +
192+
" \"thrid_message\": \"Bad format 5\"\n" +
193+
" }\n" +
194+
" ]\n" +
195+
" },\n" +
196+
" \"errors\": []\n" +
197+
" }\n" +
198+
"}";
199+
200+
EasyPostException exception = assertThrows(EasyPostException.class,
201+
() -> handleAPIError(json, 400));
202+
203+
assertEquals("Bad format 1, Bad format 2, Bad format 3, Bad format 4, Bad format 5",
204+
exception.getMessage());
205+
}
133206
}

0 commit comments

Comments
 (0)