Validation¶
Diana validates request DTOs declaratively: constraint annotations on fields become a generated validator that runs before your handler. An invalid body never reaches your code — the client gets a structured 400.
Declaring constraints¶
Annotate the fields of any class used with @Body(). The library needs a
part directive (the validator is generated into it):
import 'package:diana/diana.dart';
part 'signup_request.diana.dart';
class SignupRequest {
@NotBlank(message: 'name is required')
@Size(min: 2, max: 50)
final String name;
@Email()
final String email;
@Min(18)
@Max(130)
final int? age; // optional: constraints skip null
@Pattern(r'^[a-z0-9_]+$', message: 'lowercase letters, digits and _ only')
final String handle;
@Valid()
final Address? address; // cascade into a nested DTO
// constructor + fromJson/toJson ...
}
The constraint catalogue¶
| Annotation | Applies to | Fails when |
|---|---|---|
@NotNull({message}) |
any field | value is null |
@NotEmpty({message}) |
String, List, Set, Map |
empty |
@NotBlank({message}) |
String |
empty or whitespace-only |
@Min(value, {message}) |
numeric | < value |
@Max(value, {message}) |
numeric | > value |
@Size({min, max, message}) |
String, List, Set, Map |
length outside [min, max] |
@Pattern(regex, {message}) |
String |
doesn't match the regex (full or partial per your regex anchors) |
@Email({message}) |
String |
not shaped like an email address |
@Valid() |
a DTO field, or List of DTOs |
delegates to the nested type's validator |
Every constraint takes an optional message that replaces the default.
Applying a constraint to an incompatible field type (@Min on a String)
is a build error.
Null semantics¶
All constraints except @NotNull skip null values. Express
requiredness with a non-nullable Dart type (enforced by your fromJson)
or with @NotNull. This matches Jakarta Bean Validation semantics: @Min(1)
int? priority means "if present, at least 1".
Cascading with @Valid¶
@Valid descends into a nested DTO (or each element of a List of DTOs).
Nested field errors are prefixed with the path:
address.street → from @Valid() final Address? address;
items[2].quantity → from @Valid() final List<OrderItem> items;
The nested type must itself carry constraint annotations — @Valid into an
unconstrained class is a build error.
The error response¶
A failed validation throws ValidationException, which the pipeline maps
to:
HTTP 400
{
"error": "validation_failed",
"fields": {
"name": ["name is required", "size must be between 2 and 50"],
"email": ["must be a valid email address"]
}
}
Messages per field appear in declaration order.
What gets generated¶
For each constrained class, a public $XValidator in the part file:
class $SignupRequestValidator implements Validator<SignupRequest> {
const $SignupRequestValidator();
@override
ValidationResult validate(SignupRequest value) {
final errors = <FieldError>[];
checkNotBlank(value.name, 'name', errors, message: 'name is required');
checkSize(value.name, 'name', errors, min: 2, max: 50);
checkEmail(value.email, 'email', errors);
...
return ValidationResult(errors);
}
}
Generated code is one line per constraint; the semantics live in tested
runtime helpers (checkNotBlank, checkMin, …). Route adapters invoke the
validator automatically after fromJson; for List<Dto> bodies each
element is validated.
Validating manually¶
Validators are ordinary classes — use them anywhere:
final result = const $SignupRequestValidator().validate(request);
if (!result.isValid) { ... } // inspect result.errors
result.throwIfInvalid(); // or throw the standard 400
Default messages: must not be null, must not be empty, must not be
blank, must be at least X, must be at most X, size must be between X
and Y / at least X / at most X, must match pattern R, must be a
valid email address.
The runtime types (Validator, ValidationResult, FieldError,
ValidationException, the check* helpers) are documented in the
runtime reference.