Constraints
制約は、@NotBlank() や @Min(18) のように、プロパティに対するルールを 1 つ表すアノテーションです。
なぜ必要か
制約を使うと、ルールが守るべきデータのすぐ隣に置かれます。1 つの宣言が、そのクラスを検証するすべての場所で使われます。フォーム、リクエストボディ、メソッド引数などです。バリデータは破られたルールごとに制約の名前をエラーコードとして報告するので、messages.properties でメッセージの文言を決められます。
制約を付けられる場所:
@Introspected()クラスのプロパティ- プロパティ同士を比べるルールのための、クラスそのもの(独自の制約)
@Validated()Bean のパラメータやメソッド(メソッドバリデーション を参照)
すべての制約に共通するルール
- ほとんどの制約は
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が投げられます。
// 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)。 |
文字列がメールアドレスの形をしていることを求めます。形だけを確認し、アドレスが実在するかは確認しません。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;
}
重要な違い:
- クラス名:
@MustBeNullは@Null、@Matchesは@Pattern、@FutureDateは@Futureに当たります。エラーコードは Jakarta の名前です。 - コンテナ要素の制約は、型引数へのアノテーションではなく
@Each([...])と@EachKey([...])で書きます。 - 独自の制約は
isValidメソッドを持つ 1 つのクラスです。Java ではアノテーションと別のConstraintValidatorクラスが必要です。 @Matchesは Dart のRegExp構文を使います。フラグは Dart にあるオプションに限られます。- 日付の制約は
DateTimeに対して働きます。午前 0 時ちょうどのDateTimeは日付(LocalDate)として扱われます。 - グループ型に付ける
@GroupSequenceはBeanValidator(groupSequences: ...)に渡します。