みなさんこんにちは!ヒロポンです!
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 で実機確認):

未入力が null になるので、[Required] がちゃんと弾けます。この「値型は int? にする」感覚、DB の未入力の扱いと同じ発想です。
③ ModelState.AddModelError のキーがズレて、エラーが出ない
AddModelError("Nama", ...) のようにキー名をタイプミスすると、ValidationMessageFor(m => m.Name) と紐づきません。結果、エラーが画面に出ない(ValidationSummary には出る場合あり)。地味に原因を探す羽目になるやつです。
対策は、キーに文字列直書きではなく nameof(model.Name) を使う。nameof は末尾のメンバ名 "Name" に展開されるので、そのまま ModelState のキーとして一致します。タイプミスもコンパイルで弾ける。
主な検証属性の早見表
よく使う DataAnnotations 属性をまとめておきます。

正規表現での検証は 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 で経歴詳細を見る




