Method validation
Method validation checks the arguments and the return value of a bean's methods on every call, using constraint annotations on the parameters and the method.
Why you need it
A service is called from many places: controllers, scheduled jobs, other services, tests. Checking its inputs in each caller is repetitive, and one caller will forget. With method validation the service states its rules once, on its own parameters, and every call is checked, whoever makes it.
You do not need it for objects you already validate on the way in, for example a @Valid() request body. You also do not need it inside private helper methods: only calls that go through the bean's proxy are checked.
How to turn it on
- Annotate the bean class with
@Validated(). - Put constraints on parameters (they check the arguments) and on the method (they check the return value). Use
@Valid()to check the constraints inside an argument or result object. - Have a
MethodValidationPostProcessorbean. Withspring_boot_dartit is registered for you. Withspring_core_dartalone, declare it yourself.
import 'package:spring_core_dart/spring_core_dart.dart';
// Callers cannot register an empty or malformed address,
// and the service never returns a blank id.
@Service()
@Validated()
class Registration {
@NotBlank()
String register(@Email() @NotBlank() String email) => 'id-$email';
// @Valid() checks the constraints declared inside SignupForm.
void signUp(@Valid() SignupForm form) {
// ...
}
}
// Without spring_boot_dart: the bean that applies method validation.
@Configuration()
class ValidationConfig {
@Bean()
MethodValidationPostProcessor methodValidationPostProcessor(
BeanIntrospector introspector,
) => MethodValidationPostProcessor(BeanValidator(introspector: introspector));
}
Pass the context's BeanIntrospector to the BeanValidator: @Valid() arguments are application classes, and the default introspector does not know them.
What a violation looks like
When the arguments are invalid, the method does not run and the call throws a ConstraintViolationException. When the return value is invalid, the method has run, and the exception is thrown after it. For a method that returns a Future, the future completes with the exception.
const log = SpringLog('registration');
void main() {
final context = ApplicationContext.run([appBeans]);
final registration = context.getBean<Registration>();
try {
registration.register('');
} on ConstraintViolationException catch (e) {
log.warning(e.message); // register.email: must not be blank
for (final v in e.constraintViolations) {
log.info('${v.propertyPath} -> ${v.constraint.name}'); // register.email -> NotBlank
}
}
context.close();
}
Property paths start with the method name: register.email for a parameter, register.<return value> for the result, signUp.form.name inside a @Valid() argument.
The web starter does not turn a ConstraintViolationException into a 400 response by itself. If a client can cause one, handle it in an @ExceptionHandler.
Groups
@Validated takes the groups to check. On the class it sets them for every method; on a method it sets them for that method only. A method-level @Validated does not turn validation on by itself: the class needs @Validated() too.
abstract final class OnUpdate {}
@Service()
@Validated()
class Users {
// Only the constraints of the OnUpdate group are checked here.
@Validated([OnUpdate])
void update(@Valid() UserForm form) {
// ...
}
}
@Validated
| Name | Type | Default | Meaning |
|---|---|---|---|
value (positional) |
List<Type> |
const [] |
Groups to check. Empty means only the Default group. |
Groups are found on the method first, then on a declaration it overrides (the nearest first), then on the bean class.
Constraints on interfaces and superclasses
Constraints on the parameters and return value of an overridden declaration apply too: an implemented interface, a superclass, a mixin. Callers that only know the interface see the rules where they read them.
abstract interface class Accounts {
String register(@NotBlank() String email);
}
@Service()
@Validated()
class DefaultAccounts implements Accounts {
// The @NotBlank of the interface is checked on this method.
@override
String register(String email) => 'registered';
}
MethodValidationPostProcessor
The bean that applies method validation to every bean whose class is annotated @Validated(). Despite its name it is an advisor: proxies are generated at build time, and the context applies it like any other advisor.
| Name | Type | Default | Meaning |
|---|---|---|---|
validator (positional, optional) |
BeanValidator? |
BeanValidator() |
The validator that checks arguments and results. Its message source and clock apply. |
excludeFilters (positional, optional) |
List<bool Function(List<Object> classAnnotations)> |
const [] |
Tests on a bean's class annotations; a bean is skipped when one returns true. |
Other members:
| Member | Meaning |
|---|---|
advice |
The MethodValidationInterceptor that does the checks. |
order |
Ordered.lowestPrecedence: validation runs innermost, after other advice such as caching. |
spring_boot_dart registers one as methodValidationPostProcessor, using the application's BeanValidator and excluding @ConfigurationProperties classes. Declare your own to change the validator or the exclusions; Boot then backs off.
MethodValidationInterceptor is the advice itself. You only build one to apply method validation with your own pointcut.
Limits
- Only calls through the bean, as other beans receive it, are checked. A call from one method of the bean to another (
this.register(...)) is not. - On a
@ConfigurationPropertiesclass,@Validated()means "check the bound values at startup", not method validation.
In Spring (Java)
@Service
@Validated
class Registration {
@NotBlank String register(@Email @NotBlank String email) { ... }
}
@Bean
static MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
Differences that matter:
- Proxies are generated at build time, not created at run time with CGLIB or JDK proxies.
MethodValidationPostProcessoris an advisor bean, not aBeanPostProcessor; declaring it as a normal@Beanis enough.- The groups of
@Validatedare aList<Type>, such as@Validated([OnUpdate]). - For a method returning a
Future, a violation of the result completes the future with the exception.