Skip to content

Commit bd31e8d

Browse files
committed
Provide access to underlying ConstraintViolation
Closes gh-33025
1 parent 3e48498 commit bd31e8d

File tree

7 files changed

+87
-15
lines changed

7 files changed

+87
-15
lines changed

spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import org.springframework.core.ParameterNameDiscoverer;
5151
import org.springframework.core.annotation.AnnotationUtils;
5252
import org.springframework.lang.Nullable;
53+
import org.springframework.util.Assert;
5354
import org.springframework.util.function.SingletonSupplier;
5455
import org.springframework.validation.BeanPropertyBindingResult;
5556
import org.springframework.validation.BindingResult;
@@ -402,7 +403,7 @@ private MessageSourceResolvable createMessageSourceResolvable(
402403
String[] codes = this.messageCodesResolver.resolveMessageCodes(code, objectName, paramName, parameterType);
403404
Object[] arguments = this.validatorAdapter.get().getArgumentsForConstraint(objectName, paramName, descriptor);
404405

405-
return new DefaultMessageSourceResolvable(codes, arguments, violation.getMessage());
406+
return new ViolationMessageSourceResolvable(codes, arguments, violation.getMessage(), violation);
406407
}
407408

408409
private BindingResult createBindingResult(MethodParameter parameter, @Nullable Object argument) {
@@ -472,7 +473,11 @@ public void addViolation(ConstraintViolation<Object> violation) {
472473
public ParameterValidationResult build() {
473474
return new ParameterValidationResult(
474475
this.parameter, this.value, this.resolvableErrors, this.container,
475-
this.containerIndex, this.containerKey);
476+
this.containerIndex, this.containerKey,
477+
(error, sourceType) -> {
478+
Assert.isTrue(sourceType.equals(ConstraintViolation.class), "Unexpected source type");
479+
return ((ViolationMessageSourceResolvable) error).getViolation();
480+
});
476481
}
477482
}
478483

@@ -526,6 +531,24 @@ public ParameterErrors build() {
526531
}
527532

528533

534+
@SuppressWarnings("serial")
535+
private static class ViolationMessageSourceResolvable extends DefaultMessageSourceResolvable {
536+
537+
private final transient ConstraintViolation<Object> violation;
538+
539+
public ViolationMessageSourceResolvable(
540+
String[] codes, Object[] arguments, String defaultMessage, ConstraintViolation<Object> violation) {
541+
542+
super(codes, arguments, defaultMessage);
543+
this.violation = violation;
544+
}
545+
546+
public ConstraintViolation<Object> getViolation() {
547+
return this.violation;
548+
}
549+
}
550+
551+
529552
/**
530553
* Default algorithm to select an object name, as described in {@link #setObjectNameResolver}.
531554
*/

spring-context/src/main/java/org/springframework/validation/method/ParameterErrors.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ public ParameterErrors(
4747
MethodParameter parameter, @Nullable Object argument, Errors errors,
4848
@Nullable Object container, @Nullable Integer index, @Nullable Object key) {
4949

50-
super(parameter, argument, errors.getAllErrors(), container, index, key);
50+
super(parameter, argument, errors.getAllErrors(),
51+
container, index, key, (error, sourceType) -> ((FieldError) error).unwrap(sourceType));
52+
5153
this.errors = errors;
5254
}
5355

spring-context/src/main/java/org/springframework/validation/method/ParameterValidationResult.java

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import java.util.Collection;
2020
import java.util.List;
21+
import java.util.function.BiFunction;
22+
import java.util.function.Function;
2123

2224
import org.springframework.context.MessageSourceResolvable;
2325
import org.springframework.core.MethodParameter;
@@ -62,13 +64,16 @@ public class ParameterValidationResult {
6264
@Nullable
6365
private final Object containerKey;
6466

67+
private final BiFunction<MessageSourceResolvable, Class<?>, Object> sourceLookup;
68+
6569

6670
/**
6771
* Create a {@code ParameterValidationResult}.
6872
*/
6973
public ParameterValidationResult(
7074
MethodParameter param, @Nullable Object arg, Collection<? extends MessageSourceResolvable> errors,
71-
@Nullable Object container, @Nullable Integer index, @Nullable Object key) {
75+
@Nullable Object container, @Nullable Integer index, @Nullable Object key,
76+
BiFunction<MessageSourceResolvable, Class<?>, Object> sourceLookup) {
7277

7378
Assert.notNull(param, "MethodParameter is required");
7479
Assert.notEmpty(errors, "`resolvableErrors` must not be empty");
@@ -78,18 +83,36 @@ public ParameterValidationResult(
7883
this.container = container;
7984
this.containerIndex = index;
8085
this.containerKey = key;
86+
this.sourceLookup = sourceLookup;
8187
}
8288

8389
/**
8490
* Create a {@code ParameterValidationResult}.
8591
* @deprecated in favor of
86-
* {@link ParameterValidationResult#ParameterValidationResult(MethodParameter, Object, Collection, Object, Integer, Object)}
92+
* {@link ParameterValidationResult#ParameterValidationResult(MethodParameter, Object, Collection, Object, Integer, Object, Function)}
93+
*/
94+
@Deprecated(since = "6.2", forRemoval = true)
95+
public ParameterValidationResult(
96+
MethodParameter param, @Nullable Object arg, Collection<? extends MessageSourceResolvable> errors,
97+
@Nullable Object container, @Nullable Integer index, @Nullable Object key) {
98+
99+
this(param, arg, errors, container, index, key, (error, sourceType) -> {
100+
throw new IllegalArgumentException("No source object of the given type");
101+
});
102+
}
103+
104+
/**
105+
* Create a {@code ParameterValidationResult}.
106+
* @deprecated in favor of
107+
* {@link ParameterValidationResult#ParameterValidationResult(MethodParameter, Object, Collection, Object, Integer, Object, Function)}
87108
*/
88109
@Deprecated(since = "6.1.3", forRemoval = true)
89110
public ParameterValidationResult(
90111
MethodParameter param, @Nullable Object arg, Collection<? extends MessageSourceResolvable> errors) {
91112

92-
this(param, arg, errors, null, null, null);
113+
this(param, arg, errors, null, null, null, (error, sourceType) -> {
114+
throw new IllegalArgumentException("No source object of the given type");
115+
});
93116
}
94117

95118

@@ -164,6 +187,17 @@ public Object getContainerKey() {
164187
return this.containerKey;
165188
}
166189

190+
/**
191+
* Unwrap the source behind the given error. For Jakarta Bean validation the
192+
* source is a {@link jakarta.validation.ConstraintViolation}.
193+
* @param sourceType the expected source type
194+
* @return the source object of the given type
195+
* @since 6.2
196+
*/
197+
@SuppressWarnings("unchecked")
198+
public <T> T unwrap(MessageSourceResolvable error, Class<T> sourceType) {
199+
return (T) this.sourceLookup.apply(error, sourceType);
200+
}
167201

168202
@Override
169203
public boolean equals(@Nullable Object other) {

spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Set;
2323
import java.util.function.Consumer;
2424

25+
import jakarta.validation.ConstraintViolation;
2526
import jakarta.validation.Valid;
2627
import jakarta.validation.constraints.Max;
2728
import jakarta.validation.constraints.Min;
@@ -99,7 +100,7 @@ void validateArguments() {
99100
default message [must not be blank]"""));
100101

101102
assertValueResult(ex.getValueResults().get(0), 2, 3, List.of("""
102-
org.springframework.context.support.DefaultMessageSourceResolvable: \
103+
org.springframework.validation.beanvalidation.MethodValidationAdapter$ViolationMessageSourceResolvable: \
103104
codes [Max.myService#addStudent.degrees,Max.degrees,Max.int,Max]; \
104105
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
105106
codes [myService#addStudent.degrees,degrees]; arguments []; default message [degrees],2]; \
@@ -136,7 +137,7 @@ void validateReturnValue() {
136137
assertThat(ex.getAllValidationResults()).hasSize(1);
137138

138139
assertValueResult(ex.getValueResults().get(0), -1, 4, List.of("""
139-
org.springframework.context.support.DefaultMessageSourceResolvable: \
140+
org.springframework.validation.beanvalidation.MethodValidationAdapter$ViolationMessageSourceResolvable: \
140141
codes [Min.myService#getIntValue,Min,Min.int]; \
141142
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
142143
codes [myService#getIntValue]; arguments []; default message [],5]; \
@@ -204,7 +205,7 @@ void validateValueListArgument() {
204205
testArgs(target, method, new Object[] {List.of(" ")}, ex -> {
205206
assertThat(ex.getAllValidationResults()).hasSize(1);
206207
assertValueResult(ex.getValueResults().get(0), 0, " ", List.of("""
207-
org.springframework.context.support.DefaultMessageSourceResolvable: \
208+
org.springframework.validation.beanvalidation.MethodValidationAdapter$ViolationMessageSourceResolvable: \
208209
codes [NotBlank.myService#addHobbies.hobbies,NotBlank.hobbies,NotBlank.java.util.List,NotBlank]; \
209210
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
210211
codes [myService#addHobbies.hobbies,hobbies]; \
@@ -220,7 +221,7 @@ void validateValueSetArgument() {
220221
testArgs(target, method, new Object[] {Set.of("test", " ")}, ex -> {
221222
assertThat(ex.getAllValidationResults()).hasSize(1);
222223
assertValueResult(ex.getValueResults().get(0), 0, Set.of("test", " "), List.of("""
223-
org.springframework.context.support.DefaultMessageSourceResolvable: \
224+
org.springframework.validation.beanvalidation.MethodValidationAdapter$ViolationMessageSourceResolvable: \
224225
codes [NotBlank.myService#addUniqueHobbies.hobbies,NotBlank.hobbies,NotBlank.java.util.Set,NotBlank]; \
225226
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
226227
codes [myService#addUniqueHobbies.hobbies,hobbies]; \
@@ -254,9 +255,14 @@ private static void assertValueResult(
254255

255256
assertThat(result.getMethodParameter().getParameterIndex()).isEqualTo(parameterIndex);
256257
assertThat(result.getArgument()).isEqualTo(argument);
257-
assertThat(result.getResolvableErrors())
258+
259+
List<MessageSourceResolvable> resolvableErrors = result.getResolvableErrors();
260+
assertThat(resolvableErrors)
258261
.extracting(MessageSourceResolvable::toString)
259262
.containsExactlyInAnyOrderElementsOf(errors);
263+
264+
resolvableErrors.forEach(error ->
265+
assertThat(result.unwrap(error, ConstraintViolation.class)).isNotNull());
260266
}
261267

262268
private static Method getMethod(Object target, String methodName) {

spring-web/src/test/java/org/springframework/web/method/support/HandlerMethodValidationExceptionTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ private static MethodValidationResult createMethodValidationResult(HandlerMethod
128128
}
129129
else {
130130
MessageSourceResolvable error = new DefaultMessageSourceResolvable("Size");
131-
return new ParameterValidationResult(param, "123", List.of(error), null, null, null);
131+
return new ParameterValidationResult(
132+
param, "123", List.of(error), null, null, null, (e, t) -> null);
132133
}
133134
})
134135
.toList());

spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MethodValidationTests.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ void modelAttributeWithBindingResultAndRequestHeader() {
220220

221221
assertValueResult(ex.getValueResults().get(0), 2, "123", Collections.singletonList(
222222
"""
223-
org.springframework.context.support.DefaultMessageSourceResolvable: \
223+
org.springframework.validation.beanvalidation.MethodValidationAdapter$ViolationMessageSourceResolvable: \
224224
codes [Size.validController#handle.myHeader,Size.myHeader,Size.java.lang.String,Size]; \
225225
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
226226
codes [validController#handle.myHeader,myHeader]; arguments []; default message [myHeader],10,5]; \
@@ -360,9 +360,15 @@ private static void assertValueResult(
360360

361361
assertThat(result.getMethodParameter().getParameterIndex()).isEqualTo(parameterIndex);
362362
assertThat(result.getArgument()).isEqualTo(argument);
363-
assertThat(result.getResolvableErrors())
363+
364+
List<MessageSourceResolvable> resolvableErrors = result.getResolvableErrors();
365+
assertThat(resolvableErrors)
364366
.extracting(MessageSourceResolvable::toString)
365367
.containsExactlyInAnyOrderElementsOf(errors);
368+
369+
resolvableErrors.forEach(error ->
370+
assertThat(result.unwrap(error, ConstraintViolation.class)).isNotNull());
371+
366372
}
367373

368374

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ void modelAttributeWithBindingResultAndRequestHeader() {
183183
));
184184

185185
assertValueResult(ex.getValueResults().get(0), 2, "123", List.of("""
186-
org.springframework.context.support.DefaultMessageSourceResolvable: \
186+
org.springframework.validation.beanvalidation.MethodValidationAdapter$ViolationMessageSourceResolvable: \
187187
codes [Size.validController#handle.myHeader,Size.myHeader,Size.java.lang.String,Size]; \
188188
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
189189
codes [validController#handle.myHeader,myHeader]; arguments []; default message [myHeader],10,5]; \

0 commit comments

Comments
 (0)