Skip to content

Commit fc052e9

Browse files
authored
When possible, improve error messages on ENUM fields in request bodies (#101)
* Better error on JSON bodies * Trim quotes off value * Defensively code groups * Assume value is quoted
1 parent 776d7d4 commit fc052e9

File tree

4 files changed

+114
-4
lines changed

4 files changed

+114
-4
lines changed

build.savant

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ logbackVersion = "1.5.13"
2929
slf4jVersion = "2.0.13"
3030
testngVersion = "7.8.0"
3131

32-
project(group: "org.primeframework", name: "prime-mvc", version: "5.7.0", licenses: ["ApacheV2_0"]) {
32+
project(group: "org.primeframework", name: "prime-mvc", version: "5.8.0", licenses: ["ApacheV2_0"]) {
3333
workflow {
3434
fetch {
3535
// Dependency resolution order:

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

Lines changed: 35 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;
@@ -53,6 +55,9 @@
5355
* @author Brian Pontarelli
5456
*/
5557
public abstract class BaseJacksonContentHandler implements ContentHandler {
58+
private static final Pattern invalidEnumerationValue = Pattern.compile("Cannot deserialize value of type `\\S+` from \\S+ \"(\\S+)\": not one of the values accepted for Enum class: \\[(.*)]$.*",
59+
Pattern.DOTALL | Pattern.MULTILINE);
60+
5661
private static final Logger logger = LoggerFactory.getLogger(BaseJacksonContentHandler.class);
5762

5863
protected final ExpressionEvaluator expressionEvaluator;
@@ -194,6 +199,35 @@ private void addFieldError(JsonMappingException e) {
194199
}
195200
}
196201

202+
private void addFieldError(InvalidFormatException e) {
203+
// Build the path so we can make the error
204+
String field = buildField(e);
205+
String code = "[invalidJSON]";
206+
207+
// If we cannot find the field, make this a general error.
208+
if (field.equals("")) {
209+
messageStore.add(new SimpleMessage(MessageType.ERROR, code, messageProvider.getMessage(code, "unknown", "Possible conversion error", e.getMessage())));
210+
} else {
211+
String messageText = e.getMessage();
212+
Matcher matchesEnumNotValidValue = invalidEnumerationValue.matcher(messageText);
213+
String message = null;
214+
if (matchesEnumNotValidValue.matches() && matchesEnumNotValidValue.groupCount() == 2) {
215+
code = "[invalid]%s".formatted(field);
216+
// if we have an invalid enum value, provide a better message
217+
String possibleValues = matchesEnumNotValidValue.group(2);
218+
String valueUsed = matchesEnumNotValidValue.group(1);
219+
message = messageProvider.getMessage(code,
220+
valueUsed,
221+
possibleValues);
222+
}
223+
// otherwise, fall back to what we know
224+
if (message == null) {
225+
message = messageProvider.getMessage(code, field, "Possible conversion error", messageText);
226+
}
227+
messageStore.add(new SimpleFieldMessage(MessageType.ERROR, field, code, message));
228+
}
229+
}
230+
197231
private String buildField(JsonMappingException e) {
198232
StringBuilder fieldBuilder = new StringBuilder();
199233
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)