Constraints
A constraint is an annotation that states one rule for a property, such as @NotBlank() or @Min(18).
Why you need them
Constraints keep the rules next to the data they protect. One declaration serves every place that validates the class: a form, a request body, a method argument. The validator reports each broken rule with the constraint's name as the error code, so messages.properties can word the message.
Put constraints on:
- a property of an
@Introspected()class; - the class itself, for a rule that compares properties (a custom constraint);
- a parameter or a method of a
@Validated()bean (see Method validation).
Rules shared by all constraints
- Most constraints let
nullpass. Add@NotNull()when a value is required. - Every constraint takes the two named parameters below.
| Name | Type | Default | Meaning |
|---|---|---|---|
message |
String? |
null |
Text used instead of the default message. {min} placeholders take the constraint's attributes, ${validatedValue} the invalid value, {some.key} a message of the validator's validationMessageSource. |
groups |
List<Type> |
const [] |
The validation groups the constraint belongs to. Empty means the Default group. |
- A constraint on a type it cannot check (
@Sizeon anint,@NotBlankon aList) fails the build. At run time, fordynamicorObjectproperties, it throwsUnexpectedTypeException. - Invalid parameters (
Size(min: -1), a malformed regular expression) throwConstraintDeclarationException.
// A custom message with placeholders.
@Introspected()
class Profile {
@Size(max: 3, message: r'"${validatedValue}" is longer than {max}')
String code = '';
}
Presence
@NotNull
Requires a value: null fails, anything else passes.
@Introspected()
class Signup {
// The email may be missing in the input, but not in a valid signup.
@NotNull()
String? email;
}
Error code NotNull. Default message: must not be null. Parameters: message, groups.
@MustBeNull
Requires the value to be null. It is usually limited to one group, for example an id that a client must not send when creating a record.
abstract final class OnCreate {}
@Introspected()
class NewUser {
// The database assigns the id; the client must not send one.
@MustBeNull(groups: [OnCreate])
int? id;
}
Error code Null (the Jakarta name; Null is a Dart core type). Default message: must be null. Parameters: message, groups.
@NotEmpty
Requires a string, list, set or map that is neither null nor empty. For text, @NotBlank is usually better, since it also rejects ' '.
@Introspected()
class Cart {
// A cart cannot be ordered empty.
@NotEmpty()
List<String> items = [];
}
Error code NotEmpty. Default message: must not be empty. Supported types: String, Iterable, Map. Parameters: message, groups.
@NotBlank
Requires text with at least one visible character: null, '' and ' ' fail. "Visible" means a character above U+0020, as Java's String.trim() sees it.
@Introspected()
class Signup {
// A name of only spaces is not a name.
@NotBlank()
String name = '';
}
Error code NotBlank. Default message: must not be blank. Supported type: String. Parameters: message, groups.
Size
@Size
Limits the length of a string, or the number of entries of a list, set or map. Both limits are included. null passes, and Size(min: 1) does not reject ' ' (use @NotBlank).
@Introspected()
class Signup {
// A display name of 2 to 20 characters.
@Size(min: 2, max: 20)
String name = '';
// At most 5 tags.
@Size(max: 5)
List<String> tags = [];
}
| Name | Type | Default | Meaning |
|---|---|---|---|
min |
int |
0 |
Smallest allowed length, included. Must not be negative. |
max |
int |
0x7FFFFFFF |
Largest allowed length, included. The default means no limit. Must not be below min. |
message |
String? |
null |
Replaces size must be between {min} and {max}. |
groups |
List<Type> |
const [] |
Validation groups. |
Error code Size. Field error arguments: [field, max, min]. In Flutter apps the class is exported as SizeConstraint, because Flutter has its own Size class.
Numbers
The number constraints check int, double, BigInt and numeric strings. Values are compared exactly, without floating-point rounding. A string that is not a number fails, and so does double.nan. null passes.
@Min
Requires a number of at least value.
@Introspected()
class Signup {
// Only adults may sign up.
@Min(18)
int age = 0;
}
| Name | Type | Default | Meaning |
|---|---|---|---|
value (positional) |
num |
required | Smallest allowed number, included. Must be finite. |
message |
String? |
null |
Replaces must be greater than or equal to {value}. |
groups |
List<Type> |
const [] |
Validation groups. |
Error code Min. Field error arguments: [field, value].
@Max
Requires a number of at most value.
@Introspected()
class Order {
// No more than 100 copies in one order.
@Max(100)
int quantity = 1;
}
| Name | Type | Default | Meaning |
|---|---|---|---|
value (positional) |
num |
required | Largest allowed number, included. Must be finite. |
message |
String? |
null |
Replaces must be less than or equal to {value}. |
groups |
List<Type> |
const [] |
Validation groups. |
Error code Max. Field error arguments: [field, value].
@DecimalMin
Requires a number above a decimal limit, or equal to it when inclusive. Use it instead of @Min when the limit has decimals, or when the limit itself must be excluded. The limit is a string, so it is exact.
@Introspected()
class Product {
// Nothing is free: at least one cent.
@DecimalMin('0.01')
double price = 1;
}
| Name | Type | Default | Meaning |
|---|---|---|---|
value (positional) |
String |
required | The limit, such as '0.01' or '1e3'. A string that is not a number throws ConstraintDeclarationException. |
inclusive |
bool |
true |
Whether value itself passes. |
message |
String? |
null |
Replaces must be greater than or equal to {value} (or must be greater than {value} when not inclusive). |
groups |
List<Type> |
const [] |
Validation groups. |
Error code DecimalMin. Field error arguments: [field, inclusive, value].
@DecimalMax
Requires a number below a decimal limit, or equal to it when inclusive.
@Introspected()
class Discount {
// A discount below 100 percent.
@DecimalMax('100', inclusive: false)
double percent = 0;
}
| Name | Type | Default | Meaning |
|---|---|---|---|
value (positional) |
String |
required | The limit, such as '100'. |
inclusive |
bool |
true |
Whether value itself passes. |
message |
String? |
null |
Replaces must be less than or equal to {value} (or must be less than {value}). |
groups |
List<Type> |
const [] |
Validation groups. |
Error code DecimalMax. Field error arguments: [field, inclusive, value].
@Positive
Requires a number greater than 0.
@Introspected()
class Product {
// A price must be above zero.
@Positive()
double price = 1;
}
Error code Positive. Default message: must be greater than 0. Parameters: message, groups.
@PositiveOrZero
Requires a number of 0 or more.
@Introspected()
class Account {
// A balance may be empty, not overdrawn.
@PositiveOrZero()
int balance = 0;
}
Error code PositiveOrZero. Default message: must be greater than or equal to 0. Parameters: message, groups.
@Negative
Requires a number less than 0.
@Introspected()
class Refund {
// A refund is booked as a negative amount.
@Negative()
int amount = -1;
}
Error code Negative. Default message: must be less than 0. Parameters: message, groups.
@NegativeOrZero
Requires a number of 0 or less.
@Introspected()
class Adjustment {
// A correction may lower the stock or leave it as it is.
@NegativeOrZero()
int delta = 0;
}
Error code NegativeOrZero. Default message: must be less than or equal to 0. Parameters: message, groups.
@Digits
Limits how many digits a number has before and after the decimal point. Numbers and BigInts are counted without trailing zeros (1.50 has one fraction digit). Numeric strings are counted as written ('1.50' has two).
@Introspected()
class Invoice {
// Up to 999999.99, as in a DECIMAL(8, 2) column.
@Digits(integer: 6, fraction: 2)
double total = 0;
}
| Name | Type | Default | Meaning |
|---|---|---|---|
integer |
int |
required | Most digits before the decimal point. Must not be negative. |
fraction |
int |
required | Most digits after the decimal point; 0 allows whole numbers only. Must not be negative. |
message |
String? |
null |
Replaces numeric value out of bounds (<{integer} digits>.<{fraction} digits> expected). |
groups |
List<Type> |
const [] |
Validation groups. |
Error code Digits. Field error arguments: [field, fraction, integer].
Text formats
@Matches
Requires a string to match a regular expression as a whole, like Java's Matcher.matches(). You do not need ^ and $. The default message shows the expression, so give a message that users understand.
@Introspected()
class Address {
// A five-digit postal code.
@Matches(r'\d{5}', message: 'must be 5 digits')
String? zip;
// Letters in any case.
@Matches('[a-z]+', flags: [PatternFlag.caseInsensitive])
String code = 'abc';
}
| Name | Type | Default | Meaning |
|---|---|---|---|
regexp (positional) |
String |
required | The expression, in Dart RegExp syntax, that the whole value must match. |
flags |
List<PatternFlag> |
const [] |
Matching options. |
message |
String? |
null |
Replaces must match "{regexp}". |
groups |
List<Type> |
const [] |
Validation groups. |
Error code Pattern (the Jakarta name; Pattern is a Dart core type). Supported type: String. Field error arguments: [field, flags, regexp].
PatternFlag values:
| Value | Meaning |
|---|---|
caseInsensitive |
Letters match regardless of case. |
multiline |
^ and $ also match at line breaks. |
dotall |
. also matches line breaks. |
unicodeCase |
Unicode-aware matching (Dart's unicode: true). |
Requires a string to look like an email address. It checks the form only, not that the address exists. A domain without a dot, such as localhost, passes. null and the empty string pass; add @NotBlank() to require an address.
@Introspected()
class Signup {
// Every user needs an address.
@NotNull()
@Email()
String? email;
// A work address must be in the company domain.
@Email(regexp: r'.+@example\.com')
String? work;
}
| Name | Type | Default | Meaning |
|---|---|---|---|
regexp |
String |
'.*' |
An extra expression the whole address must also match. |
flags |
List<PatternFlag> |
const [] |
Matching options for regexp. |
message |
String? |
null |
Replaces must be a well-formed email address. |
groups |
List<Type> |
const [] |
Validation groups. |
Error code Email. Supported type: String. Field error arguments: [field, flags, regexp].
Booleans
@AssertTrue
Requires a bool to be true. null passes.
@Introspected()
class Signup {
// The signup is refused until the terms are accepted.
@AssertTrue(message: 'terms must be accepted')
bool acceptedTerms = false;
}
Error code AssertTrue. Default message: must be true. Supported type: bool. Parameters: message, groups.
@AssertFalse
Requires a bool to be false. null passes.
@Introspected()
class Account {
// A new account cannot start locked.
@AssertFalse()
bool locked = false;
}
Error code AssertFalse. Default message: must be false. Supported type: bool. Parameters: message, groups.
Dates
The date constraints check DateTime values. "Now" comes from the validator's ClockProvider, so tests can fix it.
A DateTime without a time of day (DateTime(2026, 5, 1), or what DateTime.parse('2026-05-01') returns) is treated as a date and compared by day, like Java's LocalDate: today is neither past nor future. Other values are compared to the exact instant.
@Past
Requires a DateTime before now.
@Introspected()
class Person {
// A birthday cannot be today or later.
@Past()
DateTime? birthday;
}
Error code Past. Default message: must be a past date. Parameters: message, groups.
@PastOrPresent
Requires a DateTime before now, or now (today, for a date).
@Introspected()
class Payment {
// A payment can be booked today, not tomorrow.
@PastOrPresent()
DateTime? paidOn;
}
Error code PastOrPresent. Default message: must be a date in the past or in the present. Parameters: message, groups.
@FutureDate
Requires a DateTime after now.
@Introspected()
class Booking {
// A booking starts tomorrow at the earliest.
@FutureDate()
DateTime? startsOn;
}
Error code Future (the Jakarta name; Future is a Dart core type). Default message: must be a future date. Parameters: message, groups.
@FutureOrPresent
Requires a DateTime after now, or now (today, for a date).
@Introspected()
class Coupon {
// A coupon that expired yesterday cannot be saved.
@FutureOrPresent()
DateTime? validUntil;
}
Error code FutureOrPresent. Default message: must be a date in the present or in the future. Parameters: message, groups.
To fix "now" in tests, give the validator a ClockProvider:
// Tests run on a fixed date.
final class FixedClock implements ClockProvider {
const FixedClock(this.time);
final DateTime time;
@override
DateTime now() => time;
}
final validator = BeanValidator(
introspector: introspector,
clockProvider: FixedClock(DateTime.utc(2026, 1, 1)),
);
Nested objects and collections
@Valid
Asks the validator to check the constraints inside a value: a nested object, the objects in a list, set or map, or a method argument. Without it, constraints inside a property are not checked.
@Introspected()
class Order {
// An order is only valid with a valid address and valid items.
@Valid()
Address address = Address();
@Valid()
List<Item> items = [];
}
Errors inside a list element are reported as items[0].sku, inside a set as items[].sku, inside a map value as byKey[key].sku. null values are skipped. @Valid() has no parameters.
@Each
Checks every element of a list or set, or every value of a map, against a list of constraints. Jakarta writes these on the type argument (List<@NotBlank String>); Dart has no annotations there.
@Introspected()
class Survey {
// Short, non-blank answers.
@Each([NotBlank(), Size(max: 20)])
List<String> answers = [];
// No negative scores.
@Each([Min(0)])
Map<String, int> scores = {};
}
| Name | Type | Default | Meaning |
|---|---|---|---|
constraints (positional) |
List<Constraint> |
required | The constraints each element or map value must meet. |
A null collection is skipped. A value that is not a collection or map throws ConstraintDeclarationException. A list element is reported as answers[1].<list element> (field error on answers[1]), a map value as scores[math].<map value>.
@EachKey
Checks every key of a map against a list of constraints.
@Introspected()
class Survey {
// Subject names of at most 10 characters.
@EachKey([Size(max: 10)])
Map<String, int> scores = {};
}
| Name | Type | Default | Meaning |
|---|---|---|---|
constraints (positional) |
List<Constraint> |
required | The constraints each key must meet. |
A key is reported as scores<K>[key].<map key> (field error on scores[key]).
Groups
A group is a plain type, usually an empty abstract final class. It lets one class carry rules for different situations. A constraint without groups belongs to Default. Validating without groups checks only Default. To check the ordinary rules and a group, list both.
abstract final class OnUpdate {}
@Introspected()
class UserForm {
// Set by the server on create, required on update.
@NotNull(groups: [OnUpdate])
int? id;
@NotBlank()
String name = '';
}
// An update checks the ordinary rules and the update rules.
validator.validateObject(form, groups: [Default, OnUpdate]);
@GroupSequence
Checks the groups of a class in stages. The groups are checked in order, and checking stops at the first group with a violation. Use it when some rules are expensive or pointless while simpler ones fail. The class itself stands for its ordinary constraints and must be listed; Default must not be.
abstract final class Expensive {}
@Introspected()
@GroupSequence([Order, Expensive])
class Order {
@NotBlank()
String sku = '';
// Only checked when the ordinary constraints of Order pass.
@StockAvailable(groups: [Expensive]) // a custom constraint
String warehouse = '';
}
| Name | Type | Default | Meaning |
|---|---|---|---|
value (positional) |
List<Type> |
required | The groups in the order they are checked. |
A sequence used as a group of its own is passed to BeanValidator(groupSequences: {Complete: [Default, Strict]}) instead, since Dart group types cannot carry annotations read at run time. A wrong sequence (a cycle, Default in the list, the class missing) throws GroupDefinitionException.
@ConvertGroup
Placed next to @Valid(). When the outer validation runs group from, the nested value is checked with group to.
abstract final class DriverChecks {}
@Introspected()
class Car {
// The ordinary check of a Car checks its driver's DriverChecks rules.
@Valid()
@ConvertGroup(to: DriverChecks) // from: Default
Driver? driver;
}
| Name | Type | Default | Meaning |
|---|---|---|---|
to |
Type |
required | The group the nested value is checked with. |
from |
Type |
Default |
The group of the outer validation that is converted. |
Without @Valid() it is a declaration error.
Writing your own constraint
Extend Constraint for a rule the built-in constraints do not cover. Override name (the error code), defaultMessage and isValid. A class-level constraint receives the whole object.
// A class-level rule that compares two properties.
class PasswordsMatch extends Constraint {
const PasswordsMatch({super.message, super.groups});
@override
String get name => 'PasswordsMatch';
@override
String get defaultMessage => 'passwords must match';
@override
bool isValid(Object? value) =>
value is! SignupForm || value.password == value.confirm;
// Report the error on the `confirm` field, so a form can show it there.
@override
bool isValidWithContext(Object? value, ConstraintValidatorContext context) {
if (isValid(value)) return true;
context
..disableDefaultConstraintViolation()
..buildConstraintViolationWithTemplate(
context.defaultConstraintMessageTemplate,
).addPropertyNode('confirm').addConstraintViolation();
return false;
}
}
@Introspected()
@PasswordsMatch()
class SignupForm {
String password = '';
String confirm = '';
}
Members of Constraint you may override:
| Member | Type | Default | Meaning |
|---|---|---|---|
name |
String |
required | Error code and start of the message codes. |
defaultMessage |
String |
required | Text when message is not set. |
isValid(value) |
bool |
required | Whether the value satisfies the rule. Return true for null unless the rule is about presence. |
isValidWithContext(value, context) |
bool |
calls isValid |
Access to the clock and to custom violations. |
attributes |
Map<String, Object?> |
{} |
Settings by name; fill {name} placeholders and become field error arguments, sorted by name. |
supports(value) |
bool |
true |
Whether the type can be checked at all; false throws UnexpectedTypeException. |
checkParameters() |
void |
does nothing | Throw ConstraintDeclarationException for invalid settings. |
In Spring (Java)
class Signup {
@NotBlank @Size(min = 2, max = 20) private String name;
@Pattern(regexp = "\\d{5}") private String zip;
@Future private LocalDate startsOn;
private List<@NotBlank String> answers;
}
Differences that matter:
- Class names:
@MustBeNullis@Null,@Matchesis@Pattern,@FutureDateis@Future. The error codes are the Jakarta names. - Container element constraints are written with
@Each([...])and@EachKey([...])instead of annotations on type arguments. - A custom constraint is one class with an
isValidmethod. Java needs an annotation plus a separateConstraintValidatorclass. @Matchesuses DartRegExpsyntax. Flags are limited to the options Dart has.- The date constraints work on
DateTime. ADateTimeat midnight counts as a date (LocalDate). - A
@GroupSequenceon a group type is given toBeanValidator(groupSequences: ...).