
# 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](method-validation.md)).

## Rules shared by all constraints

- Most constraints let `null` pass. 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 (`@Size` on an `int`, `@NotBlank` on a `List`) fails the build. At run time, for `dynamic` or `Object` properties, it throws `UnexpectedTypeException`.
- Invalid parameters (`Size(min: -1)`, a malformed regular expression) throw `ConstraintDeclarationException`.

```dart
// 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.

```dart
@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.

```dart
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 `'   '`.

```dart
@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.

```dart
@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`).

```dart
@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`.

```dart
@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`.

```dart
@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.

```dart
@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`.

```dart
@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.

```dart
@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.

```dart
@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.

```dart
@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.

```dart
@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 `BigInt`s are counted without trailing zeros (`1.50` has one fraction digit). Numeric strings are counted as written (`'1.50'` has two).

```dart
@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.

```dart
@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`). |

### @Email

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.

```dart
@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.

```dart
@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.

```dart
@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.

```dart
@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).

```dart
@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.

```dart
@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).

```dart
@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`:

```dart
// 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.

```dart
@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.

```dart
@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.

```dart
@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.

```dart
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.

```dart
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`.

```dart
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.

```dart
// 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)

```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: `@MustBeNull` is `@Null`, `@Matches` is `@Pattern`, `@FutureDate` is `@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 `isValid` method. Java needs an annotation plus a separate `ConstraintValidator` class.
- `@Matches` uses Dart `RegExp` syntax. Flags are limited to the options Dart has.
- The date constraints work on `DateTime`. A `DateTime` at midnight counts as a date (`LocalDate`).
- A `@GroupSequence` on a group type is given to `BeanValidator(groupSequences: ...)`.

## See also

- [Validation overview](index.md)
- [Validator, Errors and DataBinder](validator.md)
- [Method validation](method-validation.md)
- [Configuration](../config/index.md)
