ホーム>Laravel>Laravelのバリデーション(validation)のよく使うルールと、実際に返るエラーメッセージ
Laravel

Laravelのバリデーション(validation)のよく使うルールと、実際に返るエラーメッセージ

いつもご利用ありがとうございます。
この記事にはアフィリエイト広告を含む広告が掲載されており、その広告費によって運営しています。
Amazonのアソシエイトとして、ララテックは適格販売により収入を得ています。

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'])goldThe selected plan is invalid.
nullable・url空文字エラーなし
after:today2020-01-01must be a date after today.
confirmed確認用と不一致confirmation does not match.
Password::min(8)abcmust be at least 8 characters.
Password::numbers()abcmust 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 StoreMemberRequest

app/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で一番簡単なバリデーションをする方法

誰かの参考になれば嬉しいです!

フィードバックのお願い
この記事のフィードバックがありましたらYoutubeの適当な動画にコメントしていただいたり、お問い合わせからご連絡ください。