Skip to content

Commit 13f00f7

Browse files
YongGoosephilwebb
authored andcommitted
Wrap 'error' attribute for consistent JSON serialization
Update `DefaultErrorAttributes` implementations so that errors are wrapped for consistent JSON serialization. Prior to this commit, only `ObjectError` implementations were included in the 'errors' entry. Signed-off-by: yongjunhong <[email protected]> See gh-43330
1 parent 1c991a7 commit 13f00f7

File tree

4 files changed

+136
-18
lines changed

4 files changed

+136
-18
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
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.springframework.boot.web.error;
18+
19+
import jakarta.annotation.Nullable;
20+
import org.springframework.context.MessageSourceResolvable;
21+
import org.springframework.context.support.DefaultMessageSourceResolvable;
22+
import org.springframework.util.Assert;
23+
24+
/**
25+
* A wrapper class for error objects that implements {@link MessageSourceResolvable}.
26+
* This class extends {@link DefaultMessageSourceResolvable} and delegates the
27+
* message resolution to the wrapped error object.
28+
*
29+
* @author Yongjun Hong
30+
* @since 3.5.0
31+
*/
32+
public class ErrorWrapper extends DefaultMessageSourceResolvable {
33+
34+
private final Object error;
35+
36+
/**
37+
* Create a new {@code ErrorWrapper} instance with the specified error.
38+
*
39+
* @param error the error object to wrap (must not be {@code null})
40+
*/
41+
public ErrorWrapper(Object error) {
42+
this(error, null, null, null);
43+
}
44+
45+
/**
46+
* Create a new {@code ErrorWrapper} instance with the specified error, codes,
47+
* arguments, and default message.
48+
*
49+
* @param error the error object to wrap (must not be {@code null})
50+
* @param codes the codes to be used for message resolution
51+
* @param arguments the arguments to be used for message resolution
52+
* @param defaultMessage the default message to be used if no message is found
53+
*/
54+
public ErrorWrapper(Object error, @Nullable String[] codes, @Nullable Object[] arguments, @Nullable String defaultMessage) {
55+
super(codes, arguments, defaultMessage);
56+
Assert.notNull(error, "Error must not be null");
57+
this.error = error;
58+
}
59+
60+
/**
61+
* Return the codes to be used for message resolution.
62+
*
63+
* @return the codes to be used for message resolution
64+
*/
65+
@Override
66+
public String[] getCodes() {
67+
return ((MessageSourceResolvable) this.error).getCodes();
68+
}
69+
70+
/**
71+
* Return the arguments to be used for message resolution.
72+
*
73+
* @return the arguments to be used for message resolution
74+
*/
75+
@Override
76+
public Object[] getArguments() {
77+
return ((MessageSourceResolvable) this.error).getArguments();
78+
}
79+
80+
/**
81+
* Return the default message to be used if no message is found.
82+
*
83+
* @return the default message to be used if no message is found
84+
*/
85+
@Override
86+
public String getDefaultMessage() {
87+
return ((MessageSourceResolvable) this.error).getDefaultMessage();
88+
}
89+
90+
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.Map;
2525
import java.util.Optional;
2626

27+
import org.springframework.boot.web.error.ErrorWrapper;
2728
import org.springframework.boot.web.error.ErrorAttributeOptions;
2829
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
2930
import org.springframework.core.annotation.MergedAnnotation;
@@ -32,7 +33,6 @@
3233
import org.springframework.http.HttpStatus;
3334
import org.springframework.util.StringUtils;
3435
import org.springframework.validation.BindingResult;
35-
import org.springframework.validation.ObjectError;
3636
import org.springframework.validation.method.MethodValidationResult;
3737
import org.springframework.web.bind.annotation.ResponseStatus;
3838
import org.springframework.web.reactive.function.server.ServerRequest;
@@ -48,8 +48,8 @@
4848
* <li>error - The error reason</li>
4949
* <li>exception - The class name of the root exception (if configured)</li>
5050
* <li>message - The exception message (if configured)</li>
51-
* <li>errors - Any {@link ObjectError}s from a {@link BindingResult} or
52-
* {@link MethodValidationResult} exception (if configured)</li>
51+
* <li>errors - Any validation errors wrapped in {@link ErrorWrapper}, derived from a
52+
* {@link BindingResult} or {@link MethodValidationResult} exception (if configured)</li>
5353
* <li>trace - The exception stack trace (if configured)</li>
5454
* <li>path - The URL path when the exception was raised</li>
5555
* <li>requestId - Unique ID associated with the current request</li>
@@ -61,6 +61,7 @@
6161
* @author Scott Frederick
6262
* @author Moritz Halbritter
6363
* @author Yanming Zhou
64+
* @author Yongjun Hong
6465
* @since 2.0.0
6566
* @see ErrorAttributes
6667
*/
@@ -141,10 +142,9 @@ else if (error instanceof ResponseStatusException responseStatusException) {
141142

142143
private void addMessageAndErrorsFromMethodValidationResult(Map<String, Object> errorAttributes,
143144
MethodValidationResult result) {
144-
List<ObjectError> errors = result.getAllErrors()
145+
List<ErrorWrapper> errors = result.getAllErrors()
145146
.stream()
146-
.filter(ObjectError.class::isInstance)
147-
.map(ObjectError.class::cast)
147+
.map(ErrorWrapper::new)
148148
.toList();
149149
errorAttributes.put("message",
150150
"Validation failed for method='" + result.getMethod() + "'. Error count: " + errors.size());

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
import org.springframework.boot.web.error.ErrorAttributeOptions;
3232
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
33+
import org.springframework.boot.web.error.ErrorWrapper;
3334
import org.springframework.core.Ordered;
3435
import org.springframework.core.annotation.Order;
3536
import org.springframework.http.HttpStatus;
@@ -52,8 +53,8 @@
5253
* <li>error - The error reason</li>
5354
* <li>exception - The class name of the root exception (if configured)</li>
5455
* <li>message - The exception message (if configured)</li>
55-
* <li>errors - Any {@link ObjectError}s from a {@link BindingResult} or
56-
* {@link MethodValidationResult} exception (if configured)</li>
56+
* <li>errors - Any validation errors wrapped in {@link ErrorWrapper}, derived from a
57+
* {@link BindingResult} or {@link MethodValidationResult} exception (if configured)</li>
5758
* <li>trace - The exception stack trace (if configured)</li>
5859
* <li>path - The URL path when the exception was raised</li>
5960
* </ul>
@@ -65,6 +66,7 @@
6566
* @author Scott Frederick
6667
* @author Moritz Halbritter
6768
* @author Yanming Zhou
69+
* @author Yongjun Hong
6870
* @since 2.0.0
6971
* @see ErrorAttributes
7072
*/
@@ -153,6 +155,17 @@ private void addErrorMessage(Map<String, Object> errorAttributes, WebRequest web
153155
}
154156
}
155157

158+
private void addMessageAndErrorsFromMethodValidationResult(Map<String, Object> errorAttributes,
159+
MethodValidationResult result) {
160+
List<ErrorWrapper> errors = result.getAllErrors()
161+
.stream()
162+
.map(ErrorWrapper::new)
163+
.toList();
164+
errorAttributes.put("message",
165+
"Validation failed for method='" + result.getMethod() + "'. Error count: " + errors.size());
166+
errorAttributes.put("errors", errors);
167+
}
168+
156169
private void addExceptionErrorMessage(Map<String, Object> errorAttributes, WebRequest webRequest, Throwable error) {
157170
errorAttributes.put("message", getMessage(webRequest, error));
158171
}
@@ -187,16 +200,6 @@ private void addMessageAndErrorsFromBindingResult(Map<String, Object> errorAttri
187200
result.getAllErrors());
188201
}
189202

190-
private void addMessageAndErrorsFromMethodValidationResult(Map<String, Object> errorAttributes,
191-
MethodValidationResult result) {
192-
List<ObjectError> errors = result.getAllErrors()
193-
.stream()
194-
.filter(ObjectError.class::isInstance)
195-
.map(ObjectError.class::cast)
196-
.toList();
197-
addMessageAndErrorsForValidationFailure(errorAttributes, "method='" + result.getMethod() + "'", errors);
198-
}
199-
200203
private void addMessageAndErrorsForValidationFailure(Map<String, Object> errorAttributes, String validated,
201204
List<ObjectError> errors) {
202205
errorAttributes.put("message", "Validation failed for " + validated + ". Error count: " + errors.size());

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
* @author Scott Frederick
5656
* @author Moritz Halbritter
5757
* @author Yanming Zhou
58+
* @author Yongjun Hong
5859
*/
5960
class DefaultErrorAttributesTests {
6061

@@ -326,6 +327,30 @@ void extractBindingResultErrorsExcludeMessageAndErrors() throws Exception {
326327
assertThat(attributes).doesNotContainKey("errors");
327328
}
328329

330+
@Test
331+
void extractParameterValidationResultErrors() throws Exception {
332+
Object target = "test";
333+
Method method = String.class.getMethod("substring", int.class);
334+
MethodParameter parameter = new MethodParameter(method, 0);
335+
ParameterValidationResult parameterValidationResult = new ParameterValidationResult(parameter, -1,
336+
List.of(new ObjectError("beginIndex", "beginIndex is negative")), null, null, null,
337+
(error, sourceType) -> {
338+
throw new IllegalArgumentException("No source object of the given type");
339+
});
340+
MethodValidationResult methodValidationResult = MethodValidationResult.create(target, method,
341+
List.of(parameterValidationResult));
342+
HandlerMethodValidationException ex = new HandlerMethodValidationException(methodValidationResult);
343+
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
344+
345+
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(buildServerRequest(request, ex),
346+
ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS));
347+
348+
assertThat(attributes.get("message")).asString()
349+
.isEqualTo("Validation failed for method='public java.lang.String java.lang.String.substring(int)'. Error count: 1");
350+
assertThat(attributes).containsEntry("errors",
351+
methodValidationResult.getAllErrors().stream().filter(ObjectError.class::isInstance).toList());
352+
}
353+
329354
@Test
330355
void excludeStatus() {
331356
ResponseStatusException error = new ResponseStatusException(HttpStatus.NOT_ACCEPTABLE,

0 commit comments

Comments
 (0)