Skip to content

Commit cc708e3

Browse files
committed
Better error on JSON bodies
1 parent 776d7d4 commit cc708e3

File tree

3 files changed

+108
-3
lines changed

3 files changed

+108
-3
lines changed

src/main/java/org/primeframework/mvc/content/json/BaseJacksonContentHandler.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2013-2023, Inversoft Inc., All Rights Reserved
2+
* Copyright (c) 2013-2025, Inversoft Inc., All Rights Reserved
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,8 @@
2020
import java.nio.charset.Charset;
2121
import java.nio.charset.StandardCharsets;
2222
import java.util.List;
23+
import java.util.regex.Matcher;
24+
import java.util.regex.Pattern;
2325

2426
import com.fasterxml.jackson.core.JsonParseException;
2527
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -194,6 +196,33 @@ private void addFieldError(JsonMappingException e) {
194196
}
195197
}
196198

199+
private void addFieldError(InvalidFormatException e) {
200+
// Build the path so we can make the error
201+
String field = buildField(e);
202+
String code = "[invalidJSON]";
203+
204+
// If we cannot find the field, make this a general error.
205+
if (field.equals("")) {
206+
messageStore.add(new SimpleMessage(MessageType.ERROR, code, messageProvider.getMessage(code, "unknown", "Possible conversion error", e.getMessage())));
207+
} else {
208+
Pattern stuff = Pattern.compile("Cannot deserialize value of type `\\S+` from \\S+ (\\S+): not one of the values accepted for Enum class: \\[(.*)]$.*",
209+
Pattern.DOTALL | Pattern.MULTILINE);
210+
String messageText = e.getMessage();
211+
Matcher m = stuff.matcher(messageText);
212+
String message = null;
213+
if (m.matches()) {
214+
code = "[invalid]%s".formatted(field);
215+
message = messageProvider.getMessage(code,
216+
m.group(1),
217+
m.group(2));
218+
}
219+
if (message == null) {
220+
message = messageProvider.getMessage(code, field, "Possible conversion error", messageText);
221+
}
222+
messageStore.add(new SimpleFieldMessage(MessageType.ERROR, field, code, message));
223+
}
224+
}
225+
197226
private String buildField(JsonMappingException e) {
198227
StringBuilder fieldBuilder = new StringBuilder();
199228
List<JsonMappingException.Reference> references = e.getPath();

src/test/java/org/example/domain/UserField.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2001-2017, Inversoft Inc., All Rights Reserved
2+
* Copyright (c) 2001-2025, Inversoft Inc., All Rights Reserved
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,6 +21,8 @@
2121
import java.util.Locale;
2222
import java.util.Map;
2323

24+
import org.example.action.ParameterHandlerAction.Fruit;
25+
2426
/**
2527
* This is a test user with fields.
2628
*
@@ -39,6 +41,8 @@ public class UserField {
3941

4042
public Integer favoriteYear;
4143

44+
public Fruit fruit;
45+
4246
public Integer id;
4347

4448
public Map<Integer, Integer> ids = new HashMap<>();
@@ -49,6 +53,8 @@ public class UserField {
4953

5054
public String name;
5155

56+
public Nested nested;
57+
5258
public String password;
5359

5460
public String[] securityQuestions;
@@ -63,4 +69,8 @@ public UserField() {
6369
public UserField(String name) {
6470
this.name = name;
6571
}
72+
73+
public static class Nested {
74+
public Fruit fruit;
75+
}
6676
}

src/test/java/org/primeframework/mvc/content/json/JacksonContentHandlerTest.java

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2001-2024, Inversoft Inc., All Rights Reserved
2+
* Copyright (c) 2001-2025, Inversoft Inc., All Rights Reserved
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -41,6 +41,7 @@
4141
import org.primeframework.mvc.message.l10n.MessageProvider;
4242
import org.primeframework.mvc.parameter.el.ExpressionEvaluator;
4343
import org.primeframework.mvc.validation.ValidationException;
44+
import org.testng.annotations.DataProvider;
4445
import org.testng.annotations.Test;
4546
import static org.easymock.EasyMock.createNiceMock;
4647
import static org.easymock.EasyMock.createStrictMock;
@@ -62,6 +63,71 @@
6263
public class JacksonContentHandlerTest extends PrimeBaseTest {
6364
@Inject public ExpressionEvaluator expressionEvaluator;
6465

66+
@DataProvider(name = "trueFalse")
67+
private static Object[][] getTrueFalse() {
68+
return new Object[][]{
69+
{true},
70+
{false}
71+
};
72+
}
73+
74+
@Test(dataProvider = "trueFalse")
75+
public void enum_values(boolean nested) throws IOException {
76+
Map<Class<?>, Object> additionalConfig = new HashMap<>();
77+
Map<HTTPMethod, RequestMember> requestMembers = new HashMap<>();
78+
requestMembers.put(HTTPMethod.POST, new RequestMember("jsonRequest", UserField.class));
79+
additionalConfig.put(JacksonActionConfiguration.class, new JacksonActionConfiguration(requestMembers, null, null));
80+
81+
KitchenSinkAction action = new KitchenSinkAction(null);
82+
ActionConfiguration config = new ActionConfiguration(KitchenSinkAction.class, false, null, null, null, null, null, null, null, null, null, null, null, null, null, null, Collections.emptyList(), null, additionalConfig, null, null, null, null, null);
83+
ActionInvocationStore store = createStrictMock(ActionInvocationStore.class);
84+
expect(store.getCurrent()).andReturn(
85+
new ActionInvocation(action, new ExecuteMethodConfiguration(HTTPMethod.POST, null, null), "/action", null, config));
86+
replay(store);
87+
88+
String expected = nested ? """
89+
{
90+
"nested": {
91+
"fruit": "bar"
92+
}
93+
}
94+
""" : """
95+
{
96+
"fruit": "foo"
97+
}
98+
""";
99+
100+
HTTPRequest request = new HTTPRequest();
101+
request.setInputStream(new ByteArrayInputStream(expected.getBytes()));
102+
request.setContentLength((long) expected.getBytes().length);
103+
request.setContentType("application/json");
104+
105+
MessageProvider messageProvider = createStrictMock(MessageProvider.class);
106+
expect(messageProvider.getMessage(eq(nested ? "[invalid]nested.fruit" : "[invalid]fruit"),
107+
eq(nested ? "\"bar\"" : "\"foo\""),
108+
eq("Apple, Orange"))).andReturn("Bad value");
109+
replay(messageProvider);
110+
111+
MessageStore messageStore = createStrictMock(MessageStore.class);
112+
messageStore.add(new SimpleFieldMessage(MessageType.ERROR,
113+
nested ? "nested.fruit" : "fruit",
114+
nested ? "[invalid]nested.fruit" : "[invalid]fruit",
115+
"Bad value"));
116+
replay(messageStore);
117+
118+
JacksonContentHandler handler = new JacksonContentHandler(request, store, new ObjectMapper(), expressionEvaluator, messageProvider, messageStore);
119+
try {
120+
handler.handle();
121+
fail("Should have thrown");
122+
} catch (ValidationException e) {
123+
// Expected
124+
}
125+
126+
assertNull(action.jsonRequest);
127+
128+
verify(store, messageProvider, messageStore);
129+
}
130+
65131
@Test
66132
public void handle() throws IOException {
67133
Map<Class<?>, Object> additionalConfig = new HashMap<>();

0 commit comments

Comments
 (0)