Constraints

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

なぜ必要か

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

制約を付けられる場所:

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

名前 型 デフォルト 意味
message String? null デフォルトメッセージの代わりに使う文。{min} のようなプレースホルダには制約の属性が、${validatedValue} には不正な値が、{some.key} にはバリデータの validationMessageSource のメッセージが入ります。
groups List<Type> const [] 制約が属するバリデーショングループ。空なら Default グループです。
// A custom message with placeholders.
@Introspected()
class Profile {
  @Size(max: 3, message: r'"${validatedValue}" is longer than {max}')
  String code = '';
}

存在

@NotNull

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

@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 です。

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 のほうが通常は適しています。

@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 より大きい文字を指します。

@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 を使います)。

@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 以上の数値を求めます。

@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 以下の数値を求めます。

@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 の代わりに使います。下限は文字列なので正確です。

@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 のときは上限と等しい数値も求めます。

@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 より大きい数値を求めます。

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

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

@PositiveOrZero

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

@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 より小さい数値を求めます。

@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 以下の数値を求めます。

@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 桁)。

@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 を指定してください。

@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() を加えます。

@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 は通ります。

@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 は通ります。

@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 を求めます。

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

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

@PastOrPresent

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

@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 を求めます。

@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 を求めます。

@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 を渡します。

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

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

@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 ではそこにアノテーションを書けません。

@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

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

@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 だけが検証されます。通常のルールとあるグループの両方を検証するには、両方を並べます。

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 は含めてはいけません。

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 で検証されます。

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 をオーバーライドします。クラスレベルの制約はオブジェクト全体を受け取ります。

// 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) では

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

重要な違い:

関連項目