動くコード図鑑技術記事現場の渡り方キャリア論すべての記事About
技術記事

ASP.NET MVC のフォーム入力検証 — DataAnnotations と ModelState.IsValid を業務SEが4ステップで組む

バイブス父さん
現役の業務SE
2026年7月18日11 min read
ASP.NET MVC のフォーム入力検証 — DataAnnotations と ModelState.IsValid を業務SEが4ステップで組む

みなさんこんにちは!ヒロポンです!

WinForms から ASP.NET MVC に移ってきて、最初に固まったのがこれ。「入力チェックってどこに書くんや??」

WinForms なら簡単でした。各コントロールの Validating イベントに「空だったらダメ」「桁数オーバーはダメ」を書けばいい。ところが MVC には、その Validating イベントが無い。

で、俺は最初これが分からなくて。サーバー側の検証を丸ごと省いて、画面の JavaScript だけで済ませてた時期があります。案の定、本番でおかしなデータが入って慌てて直しました。

MVC の入力検証の正解は、モデルに属性(DataAnnotations)を付けて、コントローラで ModelState.IsValid を見る。この記事は、その ASP.NET MVC バリデーションを4ステップで組む手順です。.NET Framework 4.7.2 / MVC 5 前提でいきます。

この記事でできること(完成イメージ)

組み上がると、こういう流れになります。

  • ユーザーがフォーム送信 → モデルに付けた属性で自動チェック
  • NG なら同じ画面にエラーメッセージ付きで戻る
  • OK なら保存処理へ

WinForms では Validating を「1コントロールずつ手書き」してました。MVC では「モデルに属性を宣言するだけ」で済みます。この対応関係、WinForms の Form と Razor View の対応関係 の延長で捉えると、こんな感じで腹落ちします。

前提条件・必要なもの

  • ASP.NET MVC 5(.NET Framework 4.7.2)
  • モデル・ビュー・コントローラが一通りある状態
  • クライアント側検証を使うなら jQuery Validation(MVC 5 の標準テンプレートに同梱)

この記事のコードは MVC 5(System.Web.Mvc)前提。構文は検証済みですが、実行は Windows + Visual Studio / IIS でお試しください。

Step 1: モデルのプロパティに DataAnnotations 属性を付ける

検証ルールは、モデルのプロパティに属性として宣言します。

using System.ComponentModel.DataAnnotations;

public class UserForm
{
    [Required(ErrorMessage = "名前は必須です")]
    [StringLength(20, ErrorMessage = "20文字以内で入力してください")]
    public string Name { get; set; }

    [Range(1, 120, ErrorMessage = "年齢は1〜120で入力してください")]
    public int? Age { get; set; }   // ← 値型は int? にする(後述の罠②)

    [RegularExpression(@"^\d{3}-\d{4}$", ErrorMessage = "郵便番号は 123-4567 形式で")]
    public string Zip { get; set; }
}

[Required] が必須、[StringLength] が文字数、[Range] が数値範囲、[RegularExpression] が書式。これだけで基本の入力チェックは宣言できます。いい感じに「ルールがコードの上に一覧で見える」のが属性のいいところ。

Step 2: ビューに検証メッセージの置き場を作る

Razor ビューで、各項目の下にエラーメッセージの表示場所を置きます。

@model UserForm
@using (Html.BeginForm())
{
    @Html.ValidationSummary(true)

    @Html.LabelFor(m => m.Name)
    @Html.TextBoxFor(m => m.Name)
    @Html.ValidationMessageFor(m => m.Name)

    <button type="submit">送信</button>
}

ValidationMessageFor が項目ごとのエラー、ValidationSummary が画面上部のまとめ表示。属性で宣言したエラーメッセージが、ここに自動で出ます。こんな感じで、ビュー側は置き場所を用意するだけ。

Step 3: コントローラで ModelState.IsValid を見る

ここが一番大事!!送信を受けるアクションで、ModelState.IsValid をサーバー側で必ず確認します。

[HttpPost]
public ActionResult Register(UserForm model)
{
    if (!ModelState.IsValid)
        return View(model);   // 検証NG → 同じ画面にエラー付きで戻す

    // ここに来たら検証OK。保存処理へ
    // repository.Save(model);
    return RedirectToAction("Done");
}

ModelState.IsValid は、Step 1 の属性チェックを全部通ったかどうかを表します。false なら return View(model) で入力画面に戻す。そうするとエラーメッセージ付きで再表示されます。

ここを省くと、後述の罠①(改ざんで素通り)を踏みます。クライアント側の JavaScript だけに頼らない。これが鉄則です。

Step 4: 属性で表せない業務ルールはカスタム検証で

「開始日 < 終了日」みたいに、1項目だけでは判定できないルールもありますよね。その場合は IValidatableObject を実装します。

public class PeriodForm : IValidatableObject
{
    public DateTime Start { get; set; }
    public DateTime End { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext ctx)
    {
        if (End < Start)
            yield return new ValidationResult(
                "終了日は開始日以降にしてください",
                new[] { nameof(End) });   // エラーを表示する項目名
    }
}

「重複チェック」みたいな動的な検証をサーバー側でやりたい時は、コントローラで手動エラーを足せます。

if (repository.Exists(model.Name))
    ModelState.AddModelError(nameof(model.Name), "この名前は既に使われています");

AddModelError の第1引数はプロパティ名と一致させること(後述の罠③)。ここがズレるとエラーが画面に出ません。

動作確認

わざと空欄・桁オーバー・不正な郵便番号で送信して、エラーメッセージが出るか確認します。属性で宣言したメッセージがそのまま表示されれば成功です。

動作確認メモ: この記事のコードは ASP.NET MVC 5(System.Web.Mvc)専用で、構文と API の妥当性は検証済みです。MVC ランタイムは開発機の Linux では動かせないので、実機(Windows + Visual Studio / IIS)での動作は別途お試しください。

トラブルシューティング(こんな時どうする)

① クライアント検証だけ通して、改ざんで素通りする

正直、俺がやらかしたのはこれです。jQuery Validation でブラウザ側チェックが効いてるから安心、と ModelState.IsValid を省いた。

ブラウザ側の検証は、画面をいじれば無効化できます。開発者ツールで属性を消す、直接 POST を投げる。手はいくらでもある。クライアント検証は「親切な即時フィードバック」であって、セキュリティの担保にはなりません

対策は Step 3 の通り。サーバー側で必ず ModelState.IsValid を確認する。二重に見るのが正解で、クライアント検証はあくまで補助です。

② [Required] を int に付けても、0 が通ってしまう

これも地味にハマります。[Required] は「値が無い」を弾く属性。でも値型の int は常に既定値 0 を持つので、「未入力」と「0」を区別できません。未入力でも 0 として通ってしまう。ん?Required 付けたのに 0 が通るん??ってなるやつです。

対策は、必須にしたい値型は int?(Nullable)にする

[Required(ErrorMessage = "年齢は必須です")]
public int? Age { get; set; }   // int? なら未入力が null になり Required が効く

実行結果(int Age=0 は検証を通過し、int? Age=null は弾かれる・Validator で実機確認):

Required を int と int? に付けた時の検証結果の違い(int の 0 は通り int? の null は弾かれる)

未入力が null になるので、[Required] がちゃんと弾けます。この「値型は int? にする」感覚、DB の未入力の扱いと同じ発想です。

③ ModelState.AddModelError のキーがズレて、エラーが出ない

AddModelError("Nama", ...) のようにキー名をタイプミスすると、ValidationMessageFor(m => m.Name) と紐づきません。結果、エラーが画面に出ない(ValidationSummary には出る場合あり)。地味に原因を探す羽目になるやつです。

対策は、キーに文字列直書きではなく nameof(model.Name) を使う。nameof は末尾のメンバ名 "Name" に展開されるので、そのまま ModelState のキーとして一致します。タイプミスもコンパイルで弾ける。

主な検証属性の早見表

よく使う DataAnnotations 属性をまとめておきます。

主な DataAnnotations 検証属性(Required/StringLength/Range/RegularExpression/Compare/EmailAddress)を用途と例で比較した表

正規表現での検証は C# 正規表現の業務系基本(メール / 電話 / 郵便番号) の書き方が、そのまま [RegularExpression] に流用できます。

まとめ

要点はこれだけ。

  • 検証ルールはモデルの属性(DataAnnotations)で宣言する
  • ビューに ValidationMessageFor / ValidationSummary を置く
  • コントローラで必ず ModelState.IsValid を確認(クライアント検証だけに頼らない)
  • 複数項目のルールは IValidatableObject、動的検証は AddModelError(キーは nameof
  • 必須の値型は int? にする(int だと 0 が通る)

ASP.NET MVC を体系的に押さえたいなら、MVC の解説書を1冊持っておくといいです。この手の「検証はどこに書く?」を調べる時間がまるっと浮きます。

この手順、入力フォームを作る時のテンプレにしてください。

よくある質問

Q1. クライアント検証とサーバー検証、両方要りますか?

はい、両方が基本です。クライアント検証(jQuery Validation)は入力中に即フィードバックできてUXが良く、サーバー検証(ModelState.IsValid)は改ざんを防ぐ最後の砦になります。クライアントだけだと改ざんで素通りするので、サーバー検証は必須と考えてください。

Q2. エラーメッセージを日本語にするには?

各属性の ErrorMessage プロパティに日本語を書けば、そのまま表示されます。共通のメッセージをリソースファイル(.resx)で管理して ErrorMessageResourceName で参照する方法もありますが、まずは ErrorMessage に直書きから始めるのがシンプルです。

Q3. [Required] が効かないことがあります。なぜですか?

値型(int / DateTime / bool)に [Required] を付けると、既定値(0 など)が入るため「未入力」を検出できません。int? / DateTime? のように Nullable にすると、未入力が null になって [Required] が正しく効きます。

次に読むべき記事

以上!

同じ「サーバー検証を省いて本番でやられた」経験ある人いたら、どんどんシェア待ってるぜ!!


執筆者

バイブス父さん — 業務 SE 7 年 (SIer 正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SIer の正社員からフリーランスに転じ、 複数のエージェント経由で案件を回してきた経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。

🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る

この記事のコードと手順は ぜんぶ動作検証済み。 安心して現場で試してくれ。
バイブス父さん

現役の業務SE。C# / SQL Server 保守の現場から、コードも人もキャリアも全部書く。 実体験ベース。

運営者について