
# Constraints

制約は、`@NotBlank()` や `@Min(18)` のように、プロパティに対するルールを 1 つ表すアノテーションです。

## なぜ必要か

制約を使うと、ルールが守るべきデータのすぐ隣に置かれます。1 つの宣言が、そのクラスを検証するすべての場所で使われます。フォーム、リクエストボディ、メソッド引数などです。バリデータは破られたルールごとに制約の名前をエラーコードとして報告するので、`messages.properties` でメッセージの文言を決められます。

制約を付けられる場所:

- `@Introspected()` クラスのプロパティ
- プロパティ同士を比べるルールのための、クラスそのもの(独自の制約)
- `@Validated()` Bean のパラメータやメソッド([メソッドバリデーション](method-validation.md) を参照)

## すべての制約に共通するルール

- ほとんどの制約は `null` を通します。値が必須なら `@NotNull()` を加えます。
- すべての制約は次の 2 つの名前付きパラメータを取ります。

| 名前 | 型 | デフォルト | 意味 |
| --- | --- | --- | --- |
| `message` | `String?` | `null` | デフォルトメッセージの代わりに使う文。`{min}` のようなプレースホルダには制約の属性が、`${validatedValue}` には不正な値が、`{some.key}` にはバリデータの `validationMessageSource` のメッセージが入ります。 |
| `groups` | `List<Type>` | `const []` | 制約が属するバリデーショングループ。空なら `Default` グループです。 |

- 検証できない型に付けた制約(`int` に `@Size`、`List` に `@NotBlank`)はビルドエラーになります。`dynamic` や `Object` のプロパティでは、実行時に `UnexpectedTypeException` が投げられます。
- 不正なパラメータ(`Size(min: -1)`、壊れた正規表現)では `ConstraintDeclarationException` が投げられます。

```dart
// A custom message with placeholders.
@Introspected()
class Profile {
  @Size(max: 3, message: r'"${validatedValue}" is longer than {max}')
  String code = '';
}
```

## 存在

### @NotNull

値を必須にします。`null` は失敗し、それ以外は通ります。

```dart
@Introspected()
class Signup {
  // The email may be missing in the input, but not in a valid signup.
  @NotNull()
  String? email;
}
```

エラーコード `NotNull`。デフォルトメッセージ: `must not be null`。パラメータ: `message`、`groups`。

### @MustBeNull

値が `null` であることを求めます。通常は 1 つのグループに限定して使います。たとえば、レコード作成時にクライアントが送ってはいけない `id` です。

```dart
abstract final class OnCreate {}

@Introspected()
class NewUser {
  // The database assigns the id; the client must not send one.
  @MustBeNull(groups: [OnCreate])
  int? id;
}
```

エラーコード `Null`(Jakarta の名前。`Null` は Dart のコア型です)。デフォルトメッセージ: `must be null`。パラメータ: `message`、`groups`。

### @NotEmpty

`null` でも空でもない文字列、リスト、セット、マップを求めます。テキストには `'   '` も拒否する `@NotBlank` のほうが通常は適しています。

```dart
@Introspected()
class Cart {
  // A cart cannot be ordered empty.
  @NotEmpty()
  List<String> items = [];
}
```

エラーコード `NotEmpty`。デフォルトメッセージ: `must not be empty`。対応する型: `String`、`Iterable`、`Map`。パラメータ: `message`、`groups`。

### @NotBlank

見える文字を 1 文字以上含むテキストを求めます。`null`、`''`、`'   '` は失敗します。「見える」とは、Java の `String.trim()` と同じく U+0020 より大きい文字を指します。

```dart
@Introspected()
class Signup {
  // A name of only spaces is not a name.
  @NotBlank()
  String name = '';
}
```

エラーコード `NotBlank`。デフォルトメッセージ: `must not be blank`。対応する型: `String`。パラメータ: `message`、`groups`。

## サイズ

### @Size

文字列の長さ、またはリスト、セット、マップの要素数を制限します。両端の値を含みます。`null` は通り、`Size(min: 1)` は `'  '` を拒否しません(`@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 = [];
}
```

| 名前 | 型 | デフォルト | 意味 |
| --- | --- | --- | --- |
| `min` | `int` | `0` | 許される最小の長さ(含む)。負であってはいけません。 |
| `max` | `int` | `0x7FFFFFFF` | 許される最大の長さ(含む)。デフォルトは制限なしを意味します。`min` より小さくてはいけません。 |
| `message` | `String?` | `null` | `size must be between {min} and {max}` を置き換えます。 |
| `groups` | `List<Type>` | `const []` | バリデーショングループ。 |

エラーコード `Size`。フィールドエラーの引数: `[field, max, min]`。Flutter アプリでは、Flutter 独自の `Size` クラスがあるため `SizeConstraint` という名前でエクスポートされます。

## 数値

数値の制約は `int`、`double`、`BigInt`、数値の文字列を検証します。値は浮動小数点の丸めなしに正確に比較されます。数値でない文字列は失敗し、`double.nan` も失敗します。`null` は通ります。

### @Min

`value` 以上の数値を求めます。

```dart
@Introspected()
class Signup {
  // Only adults may sign up.
  @Min(18)
  int age = 0;
}
```

| 名前 | 型 | デフォルト | 意味 |
| --- | --- | --- | --- |
| `value`(位置引数) | `num` | 必須 | 許される最小の数(含む)。有限でなければいけません。 |
| `message` | `String?` | `null` | `must be greater than or equal to {value}` を置き換えます。 |
| `groups` | `List<Type>` | `const []` | バリデーショングループ。 |

エラーコード `Min`。フィールドエラーの引数: `[field, value]`。

### @Max

`value` 以下の数値を求めます。

```dart
@Introspected()
class Order {
  // No more than 100 copies in one order.
  @Max(100)
  int quantity = 1;
}
```

| 名前 | 型 | デフォルト | 意味 |
| --- | --- | --- | --- |
| `value`(位置引数) | `num` | 必須 | 許される最大の数(含む)。有限でなければいけません。 |
| `message` | `String?` | `null` | `must be less than or equal to {value}` を置き換えます。 |
| `groups` | `List<Type>` | `const []` | バリデーショングループ。 |

エラーコード `Max`。フィールドエラーの引数: `[field, value]`。

### @DecimalMin

小数の下限より大きい数値、`inclusive` のときは下限と等しい数値も求めます。下限に小数があるときや、下限そのものを除外したいときに `@Min` の代わりに使います。下限は文字列なので正確です。

```dart
@Introspected()
class Product {
  // Nothing is free: at least one cent.
  @DecimalMin('0.01')
  double price = 1;
}
```

| 名前 | 型 | デフォルト | 意味 |
| --- | --- | --- | --- |
| `value`(位置引数) | `String` | 必須 | 下限。`'0.01'` や `'1e3'` など。数値でない文字列は `ConstraintDeclarationException` を投げます。 |
| `inclusive` | `bool` | `true` | `value` 自体を通すかどうか。 |
| `message` | `String?` | `null` | `must be greater than or equal to {value}`(含まないときは `must be greater than {value}`)を置き換えます。 |
| `groups` | `List<Type>` | `const []` | バリデーショングループ。 |

エラーコード `DecimalMin`。フィールドエラーの引数: `[field, inclusive, value]`。

### @DecimalMax

小数の上限より小さい数値、`inclusive` のときは上限と等しい数値も求めます。

```dart
@Introspected()
class Discount {
  // A discount below 100 percent.
  @DecimalMax('100', inclusive: false)
  double percent = 0;
}
```

| 名前 | 型 | デフォルト | 意味 |
| --- | --- | --- | --- |
| `value`(位置引数) | `String` | 必須 | 上限。`'100'` など。 |
| `inclusive` | `bool` | `true` | `value` 自体を通すかどうか。 |
| `message` | `String?` | `null` | `must be less than or equal to {value}`(または `must be less than {value}`)を置き換えます。 |
| `groups` | `List<Type>` | `const []` | バリデーショングループ。 |

エラーコード `DecimalMax`。フィールドエラーの引数: `[field, inclusive, value]`。

### @Positive

0 より大きい数値を求めます。

```dart
@Introspected()
class Product {
  // A price must be above zero.
  @Positive()
  double price = 1;
}
```

エラーコード `Positive`。デフォルトメッセージ: `must be greater than 0`。パラメータ: `message`、`groups`。

### @PositiveOrZero

0 以上の数値を求めます。

```dart
@Introspected()
class Account {
  // A balance may be empty, not overdrawn.
  @PositiveOrZero()
  int balance = 0;
}
```

エラーコード `PositiveOrZero`。デフォルトメッセージ: `must be greater than or equal to 0`。パラメータ: `message`、`groups`。

### @Negative

0 より小さい数値を求めます。

```dart
@Introspected()
class Refund {
  // A refund is booked as a negative amount.
  @Negative()
  int amount = -1;
}
```

エラーコード `Negative`。デフォルトメッセージ: `must be less than 0`。パラメータ: `message`、`groups`。

### @NegativeOrZero

0 以下の数値を求めます。

```dart
@Introspected()
class Adjustment {
  // A correction may lower the stock or leave it as it is.
  @NegativeOrZero()
  int delta = 0;
}
```

エラーコード `NegativeOrZero`。デフォルトメッセージ: `must be less than or equal to 0`。パラメータ: `message`、`groups`。

### @Digits

数値の小数点の前と後の桁数を制限します。数値と `BigInt` は末尾のゼロを除いて数えます(`1.50` の小数部は 1 桁)。数値の文字列は書かれたとおりに数えます(`'1.50'` は 2 桁)。

```dart
@Introspected()
class Invoice {
  // Up to 999999.99, as in a DECIMAL(8, 2) column.
  @Digits(integer: 6, fraction: 2)
  double total = 0;
}
```

| 名前 | 型 | デフォルト | 意味 |
| --- | --- | --- | --- |
| `integer` | `int` | 必須 | 小数点より前の最大桁数。負であってはいけません。 |
| `fraction` | `int` | 必須 | 小数点より後の最大桁数。`0` は整数のみを許します。負であってはいけません。 |
| `message` | `String?` | `null` | `numeric value out of bounds (<{integer} digits>.<{fraction} digits> expected)` を置き換えます。 |
| `groups` | `List<Type>` | `const []` | バリデーショングループ。 |

エラーコード `Digits`。フィールドエラーの引数: `[field, fraction, integer]`。

## テキストの形式

### @Matches

文字列全体が正規表現に一致することを求めます。Java の `Matcher.matches()` と同じです。`^` と `$` は不要です。デフォルトメッセージには正規表現がそのまま表示されるので、ユーザーにわかる `message` を指定してください。

```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';
}
```

| 名前 | 型 | デフォルト | 意味 |
| --- | --- | --- | --- |
| `regexp`(位置引数) | `String` | 必須 | 値全体が一致すべき Dart の `RegExp` 構文の正規表現。 |
| `flags` | `List<PatternFlag>` | `const []` | マッチのオプション。 |
| `message` | `String?` | `null` | `must match "{regexp}"` を置き換えます。 |
| `groups` | `List<Type>` | `const []` | バリデーショングループ。 |

エラーコード `Pattern`(Jakarta の名前。`Pattern` は Dart のコア型です)。対応する型: `String`。フィールドエラーの引数: `[field, flags, regexp]`。

`PatternFlag` の値:

| 値 | 意味 |
| --- | --- |
| `caseInsensitive` | 大文字と小文字を区別しません。 |
| `multiline` | `^` と `$` が改行位置にも一致します。 |
| `dotall` | `.` が改行にも一致します。 |
| `unicodeCase` | Unicode を考慮したマッチ(Dart の `unicode: true`)。 |

### @Email

文字列がメールアドレスの形をしていることを求めます。形だけを確認し、アドレスが実在するかは確認しません。`localhost` のようにドットのないドメインも通ります。`null` と空文字列は通ります。アドレスを必須にするには `@NotBlank()` を加えます。

```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;
}
```

| 名前 | 型 | デフォルト | 意味 |
| --- | --- | --- | --- |
| `regexp` | `String` | `'.*'` | アドレス全体がさらに一致すべき正規表現。 |
| `flags` | `List<PatternFlag>` | `const []` | `regexp` のマッチのオプション。 |
| `message` | `String?` | `null` | `must be a well-formed email address` を置き換えます。 |
| `groups` | `List<Type>` | `const []` | バリデーショングループ。 |

エラーコード `Email`。対応する型: `String`。フィールドエラーの引数: `[field, flags, regexp]`。

## 真偽値

### @AssertTrue

`bool` が `true` であることを求めます。`null` は通ります。

```dart
@Introspected()
class Signup {
  // The signup is refused until the terms are accepted.
  @AssertTrue(message: 'terms must be accepted')
  bool acceptedTerms = false;
}
```

エラーコード `AssertTrue`。デフォルトメッセージ: `must be true`。対応する型: `bool`。パラメータ: `message`、`groups`。

### @AssertFalse

`bool` が `false` であることを求めます。`null` は通ります。

```dart
@Introspected()
class Account {
  // A new account cannot start locked.
  @AssertFalse()
  bool locked = false;
}
```

エラーコード `AssertFalse`。デフォルトメッセージ: `must be false`。対応する型: `bool`。パラメータ: `message`、`groups`。

## 日付

日付の制約は `DateTime` の値を検証します。「現在」はバリデータの `ClockProvider` から取得するので、テストで固定できます。

時刻のない `DateTime`(`DateTime(2026, 5, 1)` や `DateTime.parse('2026-05-01')` の結果)は日付として扱われ、Java の `LocalDate` と同じく日単位で比較されます。今日は過去でも未来でもありません。それ以外の値は正確な時点と比較されます。

### @Past

現在より前の `DateTime` を求めます。

```dart
@Introspected()
class Person {
  // A birthday cannot be today or later.
  @Past()
  DateTime? birthday;
}
```

エラーコード `Past`。デフォルトメッセージ: `must be a past date`。パラメータ: `message`、`groups`。

### @PastOrPresent

現在より前、または現在(日付なら今日)の `DateTime` を求めます。

```dart
@Introspected()
class Payment {
  // A payment can be booked today, not tomorrow.
  @PastOrPresent()
  DateTime? paidOn;
}
```

エラーコード `PastOrPresent`。デフォルトメッセージ: `must be a date in the past or in the present`。パラメータ: `message`、`groups`。

### @FutureDate

現在より後の `DateTime` を求めます。

```dart
@Introspected()
class Booking {
  // A booking starts tomorrow at the earliest.
  @FutureDate()
  DateTime? startsOn;
}
```

エラーコード `Future`(Jakarta の名前。`Future` は Dart のコア型です)。デフォルトメッセージ: `must be a future date`。パラメータ: `message`、`groups`。

### @FutureOrPresent

現在より後、または現在(日付なら今日)の `DateTime` を求めます。

```dart
@Introspected()
class Coupon {
  // A coupon that expired yesterday cannot be saved.
  @FutureOrPresent()
  DateTime? validUntil;
}
```

エラーコード `FutureOrPresent`。デフォルトメッセージ: `must be a date in the present or in the future`。パラメータ: `message`、`groups`。

テストで「現在」を固定するには、バリデータに `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)),
);
```

## ネストしたオブジェクトとコレクション

### @Valid

値の内部の制約を検証するようバリデータに指示します。ネストしたオブジェクト、リスト・セット・マップ内のオブジェクト、メソッド引数などです。これがないと、プロパティの内部の制約は検証されません。

```dart
@Introspected()
class Order {
  // An order is only valid with a valid address and valid items.
  @Valid()
  Address address = Address();

  @Valid()
  List<Item> items = [];
}
```

リスト要素内のエラーは `items[0].sku`、セット内は `items[].sku`、マップの値の中は `byKey[key].sku` として報告されます。`null` の値はスキップされます。`@Valid()` にパラメータはありません。

### @Each

リストやセットのすべての要素、またはマップのすべての値を、制約のリストで検証します。Jakarta では型引数に書きます(`List<@NotBlank String>`)が、Dart ではそこにアノテーションを書けません。

```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 = {};
}
```

| 名前 | 型 | デフォルト | 意味 |
| --- | --- | --- | --- |
| `constraints`(位置引数) | `List<Constraint>` | 必須 | 各要素またはマップの各値が満たすべき制約。 |

`null` のコレクションはスキップされます。コレクションでもマップでもない値は `ConstraintDeclarationException` を投げます。リスト要素は `answers[1].<list element>`(フィールドエラーは `answers[1]`)、マップの値は `scores[math].<map value>` として報告されます。

### @EachKey

マップのすべてのキーを制約のリストで検証します。

```dart
@Introspected()
class Survey {
  // Subject names of at most 10 characters.
  @EachKey([Size(max: 10)])
  Map<String, int> scores = {};
}
```

| 名前 | 型 | デフォルト | 意味 |
| --- | --- | --- | --- |
| `constraints`(位置引数) | `List<Constraint>` | 必須 | 各キーが満たすべき制約。 |

キーは `scores<K>[key].<map key>`(フィールドエラーは `scores[key]`)として報告されます。

## グループ

グループはただの型で、通常は空の `abstract final class` です。1 つのクラスに状況ごとのルールを持たせられます。`groups` のない制約は `Default` に属します。グループを指定せずに検証すると `Default` だけが検証されます。通常のルールとあるグループの両方を検証するには、両方を並べます。

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

クラスのグループを段階的に検証します。グループは順に検証され、違反のあったグループで検証が止まります。一部のルールが重い場合や、単純なルールが失敗している間は意味がない場合に使います。クラス自身が通常の制約を表し、リストに含める必要があります。`Default` は含めてはいけません。

```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 = '';
}
```

| 名前 | 型 | デフォルト | 意味 |
| --- | --- | --- | --- |
| `value`(位置引数) | `List<Type>` | 必須 | 検証する順のグループ。 |

それ自体を 1 つのグループとして使うシーケンスは、代わりに `BeanValidator(groupSequences: {Complete: [Default, Strict]})` に渡します。Dart のグループ型は実行時に読めるアノテーションを持てないためです。誤ったシーケンス(循環、リスト内の `Default`、クラス自身の欠落)は `GroupDefinitionException` を投げます。

### @ConvertGroup

`@Valid()` の隣に置きます。外側の検証がグループ `from` を実行するとき、ネストした値はグループ `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;
}
```

| 名前 | 型 | デフォルト | 意味 |
| --- | --- | --- | --- |
| `to` | `Type` | 必須 | ネストした値を検証するグループ。 |
| `from` | `Type` | `Default` | 変換される外側の検証のグループ。 |

`@Valid()` がないと宣言エラーになります。

## 独自の制約を書く

組み込みの制約でカバーできないルールには `Constraint` を継承します。`name`(エラーコード)、`defaultMessage`、`isValid` をオーバーライドします。クラスレベルの制約はオブジェクト全体を受け取ります。

```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 = '';
}
```

オーバーライドできる `Constraint` のメンバー:

| メンバー | 型 | デフォルト | 意味 |
| --- | --- | --- | --- |
| `name` | `String` | 必須 | エラーコードで、メッセージコードの先頭部分。 |
| `defaultMessage` | `String` | 必須 | `message` が指定されていないときの文。 |
| `isValid(value)` | `bool` | 必須 | 値がルールを満たすかどうか。存在に関するルールでなければ、`null` には `true` を返します。 |
| `isValidWithContext(value, context)` | `bool` | `isValid` を呼ぶ | 時計と独自の違反報告にアクセスできます。 |
| `attributes` | `Map<String, Object?>` | `{}` | 名前付きの設定値。`{name}` プレースホルダを埋め、名前順にフィールドエラーの引数になります。 |
| `supports(value)` | `bool` | `true` | その型を検証できるかどうか。`false` なら `UnexpectedTypeException` が投げられます。 |
| `checkParameters()` | `void` | 何もしない | 不正な設定に対して `ConstraintDeclarationException` を投げます。 |

## 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;
}
```

重要な違い:

- クラス名: `@MustBeNull` は `@Null`、`@Matches` は `@Pattern`、`@FutureDate` は `@Future` に当たります。エラーコードは Jakarta の名前です。
- コンテナ要素の制約は、型引数へのアノテーションではなく `@Each([...])` と `@EachKey([...])` で書きます。
- 独自の制約は `isValid` メソッドを持つ 1 つのクラスです。Java ではアノテーションと別の `ConstraintValidator` クラスが必要です。
- `@Matches` は Dart の `RegExp` 構文を使います。フラグは Dart にあるオプションに限られます。
- 日付の制約は `DateTime` に対して働きます。午前 0 時ちょうどの `DateTime` は日付(`LocalDate`)として扱われます。
- グループ型に付ける `@GroupSequence` は `BeanValidator(groupSequences: ...)` に渡します。

## 関連項目

- [バリデーションの概要](index.md)
- [Validator, Errors and DataBinder](validator.md)
- [メソッドバリデーション](method-validation.md)
- [Configuration](../config/index.md)
