Laravelのバリデーション(validation)のよく使うルールと、実際に返るエラーメッセージ
目次
Laravelのバリデーション(validate・FormRequest)でよく使うルールを並べ、Laravel 13で実際に返ってきたエラーメッセージと一緒にまとめました。配列のチェック、bail、messages()での日本語化、validated()の中身まで確かめています。
Laravel のバリデーションで、よく使うルールをまとめました。
ルールの一覧は公式ドキュメントにありますが、「このルールで引っかかると、どんなメッセージが返ってくるのか?」は、一覧を見ても分かりにくいです。
そこで、実際にリクエストを送って、返ってきたエラーメッセージをそのまま載せています。
確かめた環境は次のとおりです(2026年11月)。
- Docker の php:8.4-cli(PHP 8.4.27)
- Laravel 13.40.0(
composer create-projectで作ったまま、データベースは SQLite) php artisan serveで動かし、curlで JSON を送信
$request->validate() にルールを並べる
一番手軽なのは、ルートやコントローラーの中で $request->validate() を呼ぶ書き方です。
今回は、会員登録を想定したルールをまとめて書きました。
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Password;
Route::post('/valid/basic', function (Request $request) {
$validated = $request->validate([
'name' => ['required', 'string', 'max:20'],
'email' => ['required', 'email', 'unique:users,email'],
'age' => ['required', 'integer', 'between:0,120'],
'plan' => ['required', Rule::in(['free', 'pro'])],
'website' => ['nullable', 'url'],
'start_date' => ['required', 'date', 'after:today'],
'password' => ['required', 'confirmed', Password::min(8)->letters()->numbers()],
]);
return response()->json(['validated' => $validated]);
});ルールは 'required|string|max:20' のように | でつないだ文字列でも書けます。
ただ、Password::min() のようなルールのオブジェクトを混ぜるときは配列で書くことになるので、最初から配列で書いておくと迷いません。
何も送らなかったとき(required)
空の JSON を送ると、required を付けた項目にエラーが返りました。
curl -X POST http://localhost:8000/valid/basic \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-d '{}'{
"message": "The name field is required. (and 5 more errors)",
"errors": {
"name": [
"The name field is required."
],
"email": [
"The email field is required."
],
"age": [
"The age field is required."
],
"plan": [
"The plan field is required."
],
"start_date": [
"The start date field is required."
],
"password": [
"The password field is required."
]
}
}ステータスコードは 422 です。
nullable にした website だけは、送らなくてもエラーになっていません。
start_date が「start date」になっているように、項目名のアンダースコアはスペースに置き換えて表示されます。
ルールに合わない値を送ったとき
次は、わざと全部の項目を間違えて送りました。
{
"name": "あいうえおかきくけこさしすせそたちつてとな",
"email": "[email protected]",
"age": "二十",
"plan": "gold",
"website": "",
"start_date": "2020-01-01",
"password": "abc",
"password_confirmation": "abd"
}[email protected] は、先に users テーブルへ登録しておいたメールアドレスです。
返ってきたエラーはこうでした。
{
"message": "The name field must not be greater than 20 characters. (and 7 more errors)",
"errors": {
"name": [
"The name field must not be greater than 20 characters."
],
"email": [
"The email has already been taken."
],
"age": [
"The age field must be an integer."
],
"plan": [
"The selected plan is invalid."
],
"start_date": [
"The start date field must be a date after today."
],
"password": [
"The password field confirmation does not match.",
"The password field must be at least 8 characters.",
"The password field must contain at least one number."
]
}
}ルールごとに、どれが反応したかを表にしておきます。
| ルール | 送った値 | 返ってきたメッセージ |
|---|---|---|
| max:20(文字列) | 21文字の日本語 | must not be greater than 20 characters. |
| unique:users,email | 登録済みのメール | has already been taken. |
| integer | 二十 | must be an integer. |
| Rule::in(['free', 'pro']) | gold | The selected plan is invalid. |
| nullable・url | 空文字 | エラーなし |
| after:today | 2020-01-01 | must be a date after today. |
| confirmed | 確認用と不一致 | confirmation does not match. |
| Password::min(8) | abc | must be at least 8 characters. |
| Password::numbers() | abc | must contain at least one number. |
いくつか気になったところを書いておきます。
max:20 は、文字列のときはバイト数ではなく文字数で数えていました。
日本語の21文字で、ちゃんと「20文字を超えている」と判定されています。
website に空文字 "" を送ってもエラーになりませんでした。
Laravel 13 のアプリには、空文字を null に変えるミドルウェア(ConvertEmptyStringsToNull)が最初から入っています。
空文字が null になってから検証されるので、nullable で通ったということです。
password は、1つの項目で3つのエラーがまとめて返っています。
abc には英字が含まれているので、letters() のエラーは出ていません。
confirmed は、password_confirmation という名前の項目と一致するかを見るルールです。
通ったとき、validate() が返すもの
全部の項目を正しく送り、ついでにルールにない is_admin も混ぜてみました。
{
"name": "ララテック",
"email": "[email protected]",
"age": 30,
"plan": "pro",
"website": null,
"start_date": "2030-01-01",
"password": "secret123",
"password_confirmation": "secret123",
"is_admin": true
}validate() の戻り値をそのまま返すと、こうなりました。
{
"validated": {
"name": "ララテック",
"email": "[email protected]",
"age": 30,
"plan": "pro",
"website": null,
"start_date": "2030-01-01",
"password": "secret123"
}
}ルールに書いていない is_admin と password_confirmation は、戻り値に入っていません!
保存するときに $request->all() ではなく validate() の戻り値を使えば、勝手に送られた項目が紛れ込まないということです。
配列の中身をチェックする(tags.・items..name)
配列で送られてくる値は、* を使って中身の1つずつにルールを付けられます。
$validated = $request->validate([
'tags' => ['required', 'array', 'max:3'],
'tags.*' => ['string', 'distinct', 'max:10'],
'items' => ['array'],
'items.*.name' => ['required', 'string'],
'items.*.qty' => ['required', 'integer', 'min:1'],
]);タグを4つ(うち2つが重複)、商品の2つ目に名前なし・数量0を送りました。
{
"tags": ["php", "php", "laravel", "vue"],
"items": [
{"name": "りんご", "qty": 2},
{"qty": 0}
]
}{
"message": "The tags field must not have more than 3 items. (and 4 more errors)",
"errors": {
"tags": [
"The tags field must not have more than 3 items."
],
"tags.0": [
"The tags.0 field has a duplicate value."
],
"tags.1": [
"The tags.1 field has a duplicate value."
],
"items.1.name": [
"The items.1.name field is required."
],
"items.1.qty": [
"The items.1.qty field must be at least 1."
]
}
}max:3 は、配列のときは要素の数で判定されています。
同じ max でも、文字列なら文字数、配列なら要素数、と相手によって意味が変わります。
distinct は重複している両方(0番目と1番目)にエラーが付きました。
エラーのキーは items.1.name のように番号つきになるので、画面側で「何番目の行か」を出すときに使えます。
bail で最初のエラーだけにする
1つの項目に複数のルールを付けると、標準では全部のルールがチェックされます。
// bail なし
$request->validate(['code' => ['integer', 'digits:4', 'starts_with:9']]);
// bail あり
$request->validate(['code' => ['bail', 'integer', 'digits:4', 'starts_with:9']]);どちらにも "code": "abc" を送りました。
bail なしの結果です。
"code": [
"The code field must be an integer.",
"The code field must be 4 digits.",
"The code field must start with one of the following: 9."
]bail ありの結果です。
"code": [
"The code field must be an integer."
]数字ですらないのに「4桁にしてください」「9で始めてください」まで出ても意味がないので、こういうときは bail を先頭に付けると読みやすくなります。
FormRequest に分けて、メッセージを日本語にする
ルールが増えてきたら、FormRequest に分けるとコントローラーがすっきりします。
php artisan make:request StoreMemberRequestapp/Http/Requests/StoreMemberRequest.php を、こう書きました。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreMemberRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:20'],
'email' => ['required', 'email'],
];
}
public function messages(): array
{
return [
'required' => ':attributeを入力してください。',
'name.max' => ':attributeは:max文字以内で入力してください。',
'email.email' => ':attributeの形式が正しくありません。',
];
}
public function attributes(): array
{
return [
'name' => '名前',
'email' => 'メールアドレス',
];
}
}make:request で作った直後の authorize() は、return false; になっています。
そのまま送ってみると、ルールのチェックより前に止まって、403 が返りました。
{
"message": "This action is unauthorized."
}(実際の応答には、デバッグ用のスタックトレースも続いていました)
ログインの確認などを入れないなら、true にしておきます。
ルート側は、引数の型を FormRequest にするだけです。
use App\Http\Requests\StoreMemberRequest;
Route::post('/valid/member', function (StoreMemberRequest $request) {
return response()->json(['validated' => $request->validated()]);
});空で送った結果です。
{
"message": "名前を入力してください。 (and 1 more error)",
"errors": {
"name": [
"名前を入力してください。"
],
"email": [
"メールアドレスを入力してください。"
]
}
}21文字の名前と、abc というメールアドレスを送った結果です。
{
"message": "名前は20文字以内で入力してください。 (and 1 more error)",
"errors": {
"name": [
"名前は20文字以内で入力してください。"
],
"email": [
"メールアドレスの形式が正しくありません。"
]
}
}messages() のキーは、'required' のようにルール名だけにするとすべての項目に、'name.max' のように「項目.ルール」にするとその項目だけに効きます。
:attribute は attributes() で決めた名前に、:max はルールの値(20)に置き換わりました。
ただ、message の後ろの (and 1 more error) は英語のまま残っていました(^_^;)
画面に出すときは message ではなく errors の中身を使うのがよさそうです。
フォームから送ったときは、元の画面に戻って $errors に入る
ここまでは Accept: application/json を付けて送ったので、JSON の 422 が返ってきていました。
普通のフォームのように JSON を求めずに送ると、動きが変わります。
HTTP/1.1 302 Found
Location: http://localhost:8000/formエラーがあると、元の画面(直前のページ)へ 302 でリダイレクトされました。
戻った先の Blade は、こう書いておきます。
<form method="POST" action="/valid/member">
@csrf
<input name="name" value="{{ old('name') }}">
@error('name')
<p class="error">{{ $message }}</p>
@enderror
<input name="email" value="{{ old('email') }}">
@error('email')
<p class="error">{{ $message }}</p>
@enderror
<button>送信</button>
</form>名前を空、メールを abc にして送り、リダイレクト後の HTML を見るとこうなっていました(input とエラーの部分だけ抜き出しています)。
<input name="name" value="">
<p class="error">名前を入力してください。</p>
<input name="email" value="abc">
<p class="error">メールアドレスの形式が正しくありません。</p>@error の中の $message に FormRequest で決めたメッセージが入り、old('email') で入力した abc が戻っています。
エラーが表示されないときは、こちらの記事も見てみてください。
→ Laravelのバリデーションでerrorsが表示されないときの解決方法
英語のメッセージは lang/en/validation.php から来ている
ここまでの英語のメッセージは、Laravel に最初から入っている英語の翻訳です。
作ったばかりのアプリには lang ディレクトリがなく、.env は APP_LOCALE=en でした。
php artisan lang:publish を実行すると lang/en/ に validation.php などが書き出されましたが、出てきたのは英語(en)だけでした。
参考:メッセージを丸ごと日本語にするには
公式ドキュメントによると、lang の下に言語ごとのディレクトリを作り、config/app.php の locale(APP_LOCALE)をその言語にすると、その翻訳が使われるとのことです。
Localization(Laravel 公式ドキュメント)
日本語にするには、lang/ja/validation.php を自分で用意する必要がありそうです。
ルールの全部の一覧は、公式ドキュメントの「Available Validation Rules」にあります。
Validation - Available Validation Rules(Laravel 公式ドキュメント)
まとめ
よく使うルールと、実際に返ってくるメッセージをまとめました。
- 配列で書く(
Rule::in()やPassword::min()を混ぜられる) maxは文字列なら文字数、配列なら要素数- 保存には
validate()/validated()の戻り値を使う(ルールにない項目は入らない) - 1つ目のエラーで止めたいときは
bail - 日本語のメッセージは FormRequest の
messages()とattributes()で
フォームリクエストを最初から作る流れは、こちらの記事に書いています。
→ 【初心者向け】Laravelのフォームリクエストでバリデーションする方法
一番シンプルなバリデーションから始めたい方はこちらです。
→ 【初心者向け】Laravelで一番簡単なバリデーションをする方法
誰かの参考になれば嬉しいです!




