diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorWrapper.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorWrapper.java
new file mode 100644
index 000000000000..e0d7e9d021b8
--- /dev/null
+++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorWrapper.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2012-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.web.error;
+
+import jakarta.annotation.Nullable;
+import org.springframework.context.MessageSourceResolvable;
+import org.springframework.context.support.DefaultMessageSourceResolvable;
+import org.springframework.util.Assert;
+
+/**
+ * A wrapper class for error objects that implements {@link MessageSourceResolvable}.
+ * This class extends {@link DefaultMessageSourceResolvable} and delegates the
+ * message resolution to the wrapped error object.
+ *
+ * @author Yongjun Hong
+ * @since 3.5.0
+ */
+public class ErrorWrapper extends DefaultMessageSourceResolvable {
+
+ private final Object error;
+
+ /**
+ * Create a new {@code ErrorWrapper} instance with the specified error.
+ *
+ * @param error the error object to wrap (must not be {@code null})
+ */
+ public ErrorWrapper(Object error) {
+ this(error, null, null, null);
+ }
+
+ /**
+ * Create a new {@code ErrorWrapper} instance with the specified error, codes,
+ * arguments, and default message.
+ *
+ * @param error the error object to wrap (must not be {@code null})
+ * @param codes the codes to be used for message resolution
+ * @param arguments the arguments to be used for message resolution
+ * @param defaultMessage the default message to be used if no message is found
+ */
+ public ErrorWrapper(Object error, @Nullable String[] codes, @Nullable Object[] arguments, @Nullable String defaultMessage) {
+ super(codes, arguments, defaultMessage);
+ Assert.notNull(error, "Error must not be null");
+ this.error = error;
+ }
+
+ /**
+ * Return the codes to be used for message resolution.
+ *
+ * @return the codes to be used for message resolution
+ */
+ @Override
+ public String[] getCodes() {
+ return ((MessageSourceResolvable) this.error).getCodes();
+ }
+
+ /**
+ * Return the arguments to be used for message resolution.
+ *
+ * @return the arguments to be used for message resolution
+ */
+ @Override
+ public Object[] getArguments() {
+ return ((MessageSourceResolvable) this.error).getArguments();
+ }
+
+ /**
+ * Return the default message to be used if no message is found.
+ *
+ * @return the default message to be used if no message is found
+ */
+ @Override
+ public String getDefaultMessage() {
+ return ((MessageSourceResolvable) this.error).getDefaultMessage();
+ }
+
+}
diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java
index c02b439fa9d5..db8bc3f8082a 100644
--- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java
+++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java
@@ -24,6 +24,7 @@
import java.util.Map;
import java.util.Optional;
+import org.springframework.boot.web.error.ErrorWrapper;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
import org.springframework.core.annotation.MergedAnnotation;
@@ -32,7 +33,6 @@
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
-import org.springframework.validation.ObjectError;
import org.springframework.validation.method.MethodValidationResult;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.reactive.function.server.ServerRequest;
@@ -48,8 +48,8 @@
*
error - The error reason
* exception - The class name of the root exception (if configured)
* message - The exception message (if configured)
- * errors - Any {@link ObjectError}s from a {@link BindingResult} or
- * {@link MethodValidationResult} exception (if configured)
+ * errors - Any validation errors wrapped in {@link ErrorWrapper}, derived from a
+ * {@link BindingResult} or {@link MethodValidationResult} exception (if configured)
* trace - The exception stack trace (if configured)
* path - The URL path when the exception was raised
* requestId - Unique ID associated with the current request
@@ -61,6 +61,7 @@
* @author Scott Frederick
* @author Moritz Halbritter
* @author Yanming Zhou
+ * @author Yongjun Hong
* @since 2.0.0
* @see ErrorAttributes
*/
@@ -141,10 +142,9 @@ else if (error instanceof ResponseStatusException responseStatusException) {
private void addMessageAndErrorsFromMethodValidationResult(Map errorAttributes,
MethodValidationResult result) {
- List errors = result.getAllErrors()
+ List errors = result.getAllErrors()
.stream()
- .filter(ObjectError.class::isInstance)
- .map(ObjectError.class::cast)
+ .map(ErrorWrapper::new)
.toList();
errorAttributes.put("message",
"Validation failed for method='" + result.getMethod() + "'. Error count: " + errors.size());
diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java
index 9c351d633f79..fdeceb6ee62e 100644
--- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java
+++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java
@@ -30,6 +30,7 @@
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
+import org.springframework.boot.web.error.ErrorWrapper;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
@@ -52,8 +53,8 @@
* error - The error reason
* exception - The class name of the root exception (if configured)
* message - The exception message (if configured)
- * errors - Any {@link ObjectError}s from a {@link BindingResult} or
- * {@link MethodValidationResult} exception (if configured)
+ * errors - Any validation errors wrapped in {@link ErrorWrapper}, derived from a
+ * {@link BindingResult} or {@link MethodValidationResult} exception (if configured)
* trace - The exception stack trace (if configured)
* path - The URL path when the exception was raised
*
@@ -65,6 +66,7 @@
* @author Scott Frederick
* @author Moritz Halbritter
* @author Yanming Zhou
+ * @author Yongjun Hong
* @since 2.0.0
* @see ErrorAttributes
*/
@@ -153,6 +155,17 @@ private void addErrorMessage(Map errorAttributes, WebRequest web
}
}
+ private void addMessageAndErrorsFromMethodValidationResult(Map errorAttributes,
+ MethodValidationResult result) {
+ List errors = result.getAllErrors()
+ .stream()
+ .map(ErrorWrapper::new)
+ .toList();
+ errorAttributes.put("message",
+ "Validation failed for method='" + result.getMethod() + "'. Error count: " + errors.size());
+ errorAttributes.put("errors", errors);
+ }
+
private void addExceptionErrorMessage(Map errorAttributes, WebRequest webRequest, Throwable error) {
errorAttributes.put("message", getMessage(webRequest, error));
}
@@ -187,16 +200,6 @@ private void addMessageAndErrorsFromBindingResult(Map errorAttri
result.getAllErrors());
}
- private void addMessageAndErrorsFromMethodValidationResult(Map errorAttributes,
- MethodValidationResult result) {
- List errors = result.getAllErrors()
- .stream()
- .filter(ObjectError.class::isInstance)
- .map(ObjectError.class::cast)
- .toList();
- addMessageAndErrorsForValidationFailure(errorAttributes, "method='" + result.getMethod() + "'", errors);
- }
-
private void addMessageAndErrorsForValidationFailure(Map errorAttributes, String validated,
List errors) {
errorAttributes.put("message", "Validation failed for " + validated + ". Error count: " + errors.size());
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java
index f24de2fe8e5e..5ac81ae153c7 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java
@@ -55,6 +55,7 @@
* @author Scott Frederick
* @author Moritz Halbritter
* @author Yanming Zhou
+ * @author Yongjun Hong
*/
class DefaultErrorAttributesTests {
@@ -326,6 +327,30 @@ void extractBindingResultErrorsExcludeMessageAndErrors() throws Exception {
assertThat(attributes).doesNotContainKey("errors");
}
+ @Test
+ void extractParameterValidationResultErrors() throws Exception {
+ Object target = "test";
+ Method method = String.class.getMethod("substring", int.class);
+ MethodParameter parameter = new MethodParameter(method, 0);
+ ParameterValidationResult parameterValidationResult = new ParameterValidationResult(parameter, -1,
+ List.of(new ObjectError("beginIndex", "beginIndex is negative")), null, null, null,
+ (error, sourceType) -> {
+ throw new IllegalArgumentException("No source object of the given type");
+ });
+ MethodValidationResult methodValidationResult = MethodValidationResult.create(target, method,
+ List.of(parameterValidationResult));
+ HandlerMethodValidationException ex = new HandlerMethodValidationException(methodValidationResult);
+ MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
+
+ Map attributes = this.errorAttributes.getErrorAttributes(buildServerRequest(request, ex),
+ ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS));
+
+ assertThat(attributes.get("message")).asString()
+ .isEqualTo("Validation failed for method='public java.lang.String java.lang.String.substring(int)'. Error count: 1");
+ assertThat(attributes).containsEntry("errors",
+ methodValidationResult.getAllErrors().stream().filter(ObjectError.class::isInstance).toList());
+ }
+
@Test
void excludeStatus() {
ResponseStatusException error = new ResponseStatusException(HttpStatus.NOT_ACCEPTABLE,