Skip to content

Commit 977279b

Browse files
committed
Polish "Wrap 'error' attribute for consistent JSON serialization"
Polish code and extend wrapping to all error types. See gh-43330
1 parent 13f00f7 commit 977279b

File tree

6 files changed

+150
-149
lines changed

6 files changed

+150
-149
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2012-2025 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 java.util.ArrayList;
20+
import java.util.Collections;
21+
import java.util.List;
22+
import java.util.Objects;
23+
24+
import org.springframework.context.MessageSourceResolvable;
25+
import org.springframework.util.Assert;
26+
import org.springframework.util.CollectionUtils;
27+
28+
/**
29+
* A wrapper class for {@link MessageSourceResolvable} errors that is safe for JSON
30+
* serialization.
31+
*
32+
* @author Yongjun Hong
33+
* @author Phillip Webb
34+
* @since 3.5.0
35+
*/
36+
public final class Error implements MessageSourceResolvable {
37+
38+
private final MessageSourceResolvable cause;
39+
40+
/**
41+
* Create a new {@code Error} instance with the specified cause.
42+
* @param cause the error cause (must not be {@code null})
43+
*/
44+
private Error(MessageSourceResolvable cause) {
45+
Assert.notNull(cause, "'cause' must not be null");
46+
this.cause = cause;
47+
}
48+
49+
@Override
50+
public String[] getCodes() {
51+
return this.cause.getCodes();
52+
}
53+
54+
@Override
55+
public Object[] getArguments() {
56+
return this.cause.getArguments();
57+
}
58+
59+
@Override
60+
public String getDefaultMessage() {
61+
return this.cause.getDefaultMessage();
62+
}
63+
64+
/**
65+
* Return the original cause of the error.
66+
* @return the error cause
67+
*/
68+
public MessageSourceResolvable getCause() {
69+
return this.cause;
70+
}
71+
72+
@Override
73+
public boolean equals(Object obj) {
74+
if (this == obj) {
75+
return true;
76+
}
77+
if (obj == null || getClass() != obj.getClass()) {
78+
return false;
79+
}
80+
return Objects.equals(this.cause, ((Error) obj).cause);
81+
}
82+
83+
@Override
84+
public int hashCode() {
85+
return Objects.hash(this.cause);
86+
}
87+
88+
@Override
89+
public String toString() {
90+
return this.cause.toString();
91+
}
92+
93+
/**
94+
* Wrap the given errors.
95+
* @param errors the errors to wrap
96+
* @return a new Error list
97+
*/
98+
public static List<Error> wrap(List<? extends MessageSourceResolvable> errors) {
99+
if (CollectionUtils.isEmpty(errors)) {
100+
return Collections.emptyList();
101+
}
102+
List<Error> result = new ArrayList<>(errors.size());
103+
for (MessageSourceResolvable error : errors) {
104+
result.add(new Error(error));
105+
}
106+
return List.copyOf(result);
107+
}
108+
109+
}

Diff for: spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorWrapper.java

-90
This file was deleted.

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

+12-18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
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,11 +20,10 @@
2020
import java.io.StringWriter;
2121
import java.util.Date;
2222
import java.util.LinkedHashMap;
23-
import java.util.List;
2423
import java.util.Map;
2524
import java.util.Optional;
2625

27-
import org.springframework.boot.web.error.ErrorWrapper;
26+
import org.springframework.boot.web.error.Error;
2827
import org.springframework.boot.web.error.ErrorAttributeOptions;
2928
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
3029
import org.springframework.core.annotation.MergedAnnotation;
@@ -48,7 +47,7 @@
4847
* <li>error - The error reason</li>
4948
* <li>exception - The class name of the root exception (if configured)</li>
5049
* <li>message - The exception message (if configured)</li>
51-
* <li>errors - Any validation errors wrapped in {@link ErrorWrapper}, derived from a
50+
* <li>errors - Any validation errors wrapped in {@link Error}, derived from a
5251
* {@link BindingResult} or {@link MethodValidationResult} exception (if configured)</li>
5352
* <li>trace - The exception stack trace (if configured)</li>
5453
* <li>path - The URL path when the exception was raised</li>
@@ -113,19 +112,20 @@ private void handleException(Map<String, Object> errorAttributes, Throwable erro
113112
MergedAnnotation<ResponseStatus> responseStatusAnnotation, boolean includeStackTrace) {
114113
Throwable exception;
115114
if (error instanceof BindingResult bindingResult) {
116-
errorAttributes.put("message", error.getMessage());
117-
errorAttributes.put("errors", bindingResult.getAllErrors());
118115
exception = error;
116+
errorAttributes.put("message", error.getMessage());
117+
errorAttributes.put("errors", Error.wrap(bindingResult.getAllErrors()));
119118
}
120119
else if (error instanceof MethodValidationResult methodValidationResult) {
121-
addMessageAndErrorsFromMethodValidationResult(errorAttributes, methodValidationResult);
122120
exception = error;
121+
errorAttributes.put("message", getErrorMessage(methodValidationResult));
122+
errorAttributes.put("errors", Error.wrap(methodValidationResult.getAllErrors()));
123123
}
124124
else if (error instanceof ResponseStatusException responseStatusException) {
125-
errorAttributes.put("message", responseStatusException.getReason());
126125
exception = (responseStatusException.getCause() != null) ? responseStatusException.getCause() : error;
126+
errorAttributes.put("message", responseStatusException.getReason());
127127
if (exception instanceof BindingResult bindingResult) {
128-
errorAttributes.put("errors", bindingResult.getAllErrors());
128+
errorAttributes.put("errors", Error.wrap(bindingResult.getAllErrors()));
129129
}
130130
}
131131
else {
@@ -140,15 +140,9 @@ else if (error instanceof ResponseStatusException responseStatusException) {
140140
}
141141
}
142142

143-
private void addMessageAndErrorsFromMethodValidationResult(Map<String, Object> errorAttributes,
144-
MethodValidationResult result) {
145-
List<ErrorWrapper> errors = result.getAllErrors()
146-
.stream()
147-
.map(ErrorWrapper::new)
148-
.toList();
149-
errorAttributes.put("message",
150-
"Validation failed for method='" + result.getMethod() + "'. Error count: " + errors.size());
151-
errorAttributes.put("errors", errors);
143+
private String getErrorMessage(MethodValidationResult methodValidationResult) {
144+
return "Validation failed for method='%s'. Error count: %s".formatted(methodValidationResult.getMethod(),
145+
methodValidationResult.getAllErrors().size());
152146
}
153147

154148
@Override

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

+18-31
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
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,24 +20,22 @@
2020
import java.io.StringWriter;
2121
import java.util.Date;
2222
import java.util.LinkedHashMap;
23-
import java.util.List;
2423
import java.util.Map;
2524

2625
import jakarta.servlet.RequestDispatcher;
2726
import jakarta.servlet.ServletException;
2827
import jakarta.servlet.http.HttpServletRequest;
2928
import jakarta.servlet.http.HttpServletResponse;
3029

30+
import org.springframework.boot.web.error.Error;
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;
3433
import org.springframework.core.Ordered;
3534
import org.springframework.core.annotation.Order;
3635
import org.springframework.http.HttpStatus;
3736
import org.springframework.util.ObjectUtils;
3837
import org.springframework.util.StringUtils;
3938
import org.springframework.validation.BindingResult;
40-
import org.springframework.validation.ObjectError;
4139
import org.springframework.validation.method.MethodValidationResult;
4240
import org.springframework.web.context.request.RequestAttributes;
4341
import org.springframework.web.context.request.WebRequest;
@@ -53,7 +51,7 @@
5351
* <li>error - The error reason</li>
5452
* <li>exception - The class name of the root exception (if configured)</li>
5553
* <li>message - The exception message (if configured)</li>
56-
* <li>errors - Any validation errors wrapped in {@link ErrorWrapper}, derived from a
54+
* <li>errors - Any validation errors wrapped in {@link Error}, derived from a
5755
* {@link BindingResult} or {@link MethodValidationResult} exception (if configured)</li>
5856
* <li>trace - The exception stack trace (if configured)</li>
5957
* <li>path - The URL path when the exception was raised</li>
@@ -143,27 +141,27 @@ private void addErrorMessage(Map<String, Object> errorAttributes, WebRequest web
143141
BindingResult bindingResult = extractBindingResult(error);
144142
if (bindingResult != null) {
145143
addMessageAndErrorsFromBindingResult(errorAttributes, bindingResult);
144+
return;
146145
}
147-
else {
148-
MethodValidationResult methodValidationResult = extractMethodValidationResult(error);
149-
if (methodValidationResult != null) {
150-
addMessageAndErrorsFromMethodValidationResult(errorAttributes, methodValidationResult);
151-
}
152-
else {
153-
addExceptionErrorMessage(errorAttributes, webRequest, error);
154-
}
146+
MethodValidationResult methodValidationResult = extractMethodValidationResult(error);
147+
if (methodValidationResult != null) {
148+
addMessageAndErrorsFromMethodValidationResult(errorAttributes, methodValidationResult);
149+
return;
155150
}
151+
addExceptionErrorMessage(errorAttributes, webRequest, error);
152+
}
153+
154+
private void addMessageAndErrorsFromBindingResult(Map<String, Object> errorAttributes, BindingResult result) {
155+
errorAttributes.put("message", "Validation failed for object='%s'. Error count: %s"
156+
.formatted(result.getObjectName(), result.getAllErrors().size()));
157+
errorAttributes.put("errors", Error.wrap(result.getAllErrors()));
156158
}
157159

158160
private void addMessageAndErrorsFromMethodValidationResult(Map<String, Object> errorAttributes,
159161
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);
162+
errorAttributes.put("message", "Validation failed for method='%s'. Error count: %s"
163+
.formatted(result.getMethod(), result.getAllErrors().size()));
164+
errorAttributes.put("errors", Error.wrap(result.getAllErrors()));
167165
}
168166

169167
private void addExceptionErrorMessage(Map<String, Object> errorAttributes, WebRequest webRequest, Throwable error) {
@@ -195,17 +193,6 @@ protected String getMessage(WebRequest webRequest, Throwable error) {
195193
return "No message available";
196194
}
197195

198-
private void addMessageAndErrorsFromBindingResult(Map<String, Object> errorAttributes, BindingResult result) {
199-
addMessageAndErrorsForValidationFailure(errorAttributes, "object='" + result.getObjectName() + "'",
200-
result.getAllErrors());
201-
}
202-
203-
private void addMessageAndErrorsForValidationFailure(Map<String, Object> errorAttributes, String validated,
204-
List<ObjectError> errors) {
205-
errorAttributes.put("message", "Validation failed for " + validated + ". Error count: " + errors.size());
206-
errorAttributes.put("errors", errors);
207-
}
208-
209196
private BindingResult extractBindingResult(Throwable error) {
210197
if (error instanceof BindingResult bindingResult) {
211198
return bindingResult;

0 commit comments

Comments
 (0)