# ブラウザーとサーバーでの HTML フォームのバリデーション

> required、type、pattern、min でフォームを検証し、:user-invalid でエラーをスタイリングし、同じルールをフィールド定義でサーバー側にも適用します。

公開日 2026-10-25 · https://ikiform.com/ja/guides/html-form-submission/validation

`required`、`type="email"`、`min`、`max`、`pattern` など HTML 組み込みの制約を使えば、何か送られる前にブラウザーが不正な入力を止めます。そのうえで同じルールをサーバーでも適用してください。ボットやスクリプトはブラウザーを完全に素通りするからです。Ikiform ではフォームのフィールドを定義すれば、すべての送信がチェックされます。不正な送信にはフィールドごとのメッセージ付きで `422 validation_failed` が返り、`setCustomValidity` で表示できます。

例はワークショップの予約フォームです。上限のあるチーム人数、過去にできない日付、形式が 1 つの電話番号、決まった形の予約コードと、バリデーションが役に立つ種類のフィールドがそろっています。

## ここが違う

バリデーターは 2 つあり、それぞれが見るリクエストは同じではありません。ブラウザーがチェックするのは、スクリプトの有無にかかわらず、あなたのページを使う人だけです。サーバーは、エンドポイントに直接送信するボットや、ブラウザーのチェックをスキップする `form.submit()` を呼ぶ自分のコードも含め、すべてをチェックします。大事なのは、両方に同じことを言わせることです。

図: ブラウザーのバリデーションを通り、次に Ikiform のフィールドのバリデーションを通る送信

1. 人がフォームを送信すると、ブラウザーが HTML の制約をチェックします。不正なフィールドにはメッセージが表示され、何も送られません。
2. 正しい送信は Ikiform に届き、フォームのフィールド定義と照合されて、型付きの値として保存されます。
3. ボットやスクリプトはエンドポイントに直接送信するので、ブラウザーのチェックは実行されません。
4. Ikiform は同じ定義と照合し、何も保存する前に 422 で拒否します。

ブラウザーの層は人のためのものです。守りを固めるのはサーバーの層です。

## ブラウザーの層

フォーム: Book a workshop

- **お名前** (`name`, text, 必須)
- **仕事用のメールアドレス** (`email`, email, 必須)
- **参加人数** (`team_size`, number, 必須)
- **希望日** (`date`, date, 必須)
- **電話番号** (`phone`, phone)
- **予約コード** (`code`, text)

Ikiform が適用する定義です。チーム人数に 25、11 月 2 日より前の日付、`ws-12` のようなコードを試してみてください。

`workshop.html`:

```html
<form id="workshop" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="name">Your name</label>
  <input id="name" name="name" autocomplete="name" maxlength="120" required />

  <label for="email">Work email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />

  <label for="team_size">People attending</label>
  <input id="team_size" name="team_size" type="number" min="2" max="20" step="1" required />

  <label for="date">Preferred date</label>
  <input id="date" name="date" type="date" min="2026-11-02" required />

  <label for="phone">Phone</label>
  <input id="phone" name="phone" type="tel" autocomplete="tel" placeholder="+14155550123"
    pattern="\+[1-9][0-9]{6,14}" title="International format, like +14155550123" />

  <label for="code">Booking code</label>
  <input id="code" name="code" pattern="WS-[0-9]{4}" title="Codes look like WS-1234" />

  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Request the workshop</button>
</form>
```

各属性は 1 つの validity の状態に対応し、コンソールで `input.validity` から読み取れます:

| 属性 | validity の状態 | 注意 |
| --- | --- | --- |
| `required` | `valueMissing` | ラジオグループでは、1 つのラジオに `required` を付ければグループ全体に効きます |
| `type="email"`, `type="url"` | `typeMismatch` | 緩い: ブラウザーでは `a@b` がメールアドレスとして通ります |
| `pattern` | `patternMismatch` | 値全体と一致する必要があります。空の任意フィールドでは無視されます |
| `minlength`, `maxlength` | `tooShort`, `tooLong` | 人が値を編集したあとにだけチェックされます |
| `min`, `max` | `rangeUnderflow`, `rangeOverflow` | 数値、日付、時刻 |
| `step` | `stepMismatch` | `step="1"` は小数を拒否します |
| 解析できない入力 | `badInput` | 一部のブラウザーで数値フィールドに入力された文字 |
| `setCustomValidity()` | `customError` | 設定した任意のメッセージ。空に戻すまで続きます |

### 2 つのエンジンでのパターン

ブラウザーは `pattern` を `v` フラグでコンパイルします。これは多くの人が書く正規表現より厳格で、文字クラス内のリテラルの `-` は `[a-z\-]` のようにエスケープが必要です。Ikiform はフィールドの `pattern` を `u` フラグでコンパイルし、やはり値全体と照合します。上の予約コードのように両方で有効なパターンを書き、公開前にフォームのプレビューで 1 つずつテストしてください。

### エラーのスタイリング

`:invalid` はページを読み込んだ瞬間に空の必須フィールドにマッチするので、誰も入力しないうちからすべてのフィールドが赤く表示されます。`:user-invalid` は、人がフィールドを操作したか送信しようとしたあとにだけマッチします。ほとんどの場合、望ましいのはこちらです:

`workshop.html`:

```html
<style>
  #workshop :user-invalid {
    border-color: #b42318;
  }
</style>
```

## サーバーの層

上の定義をフォームの **Fields** タブに貼り付けます。以降、Ikiform は送信元にかかわらずすべての送信をこの定義で検証し、変換した値を保存します: `team_size` は数値、`phone` は区切り文字なし、`date` は `YYYY-MM-DD` です。ルールに違反した送信は拒否され、何も保存されず、どのフィールドが失敗したかが返されます:

| フィールド | 送信した値 | メッセージ |
| --- | --- | --- |
| `team_size` | `25` | `Must be at most 20` |
| `team_size` | `2.5` | `Enter a whole number` |
| `date` | `2026-10-30` | `Must be on or after 2026-11-02` |
| `phone` | `415 555 0123` | `Use international format, e.g. +14155550123` |
| `code` | `ws-12` | `Codes look like WS-1234` |
| `website` | 任意の値 | `Unknown field "website"` |

最後の行が重要です。フィールドを定義すると、Ikiform は定義にないフィールドをすべて拒否するので、名前を変えた入力欄は間違った名前で保存されずに、はっきりと失敗します。`_` で始まるフィールドとハニーポットは数に入りません。すべての型とルールは[フィールドとバリデーション](https://ikiform.com/ja/docs/fields.md)にあります。

### プレーンな送信の場合

コードは不要です。拒否されたプレーンな送信では、`422` ステータスで Ikiform のホストされたエラーページが表示され、各問題がフィールドのラベルの下に並び、送信元のページへのリンクが付きます。ブラウザーの **Back**（戻る）ボタンなら入力内容はたいてい残ります。リンクからだとフォームは空の状態で再読み込みされます。

### サーバーのエラーをフィールドに表示する

スクリプトを使う場合は、サーバーの各メッセージをブラウザー自身のエラー UI に渡します。`setCustomValidity` はそのテキストでフィールドを不正としてマークし、`reportValidity` はいつもの吹き出しで表示して、最初のフィールドにフォーカスします:

`workshop.js`:

```js
const form = document.querySelector("#workshop");

for (const control of form.elements) {
  control.addEventListener("input", () => control.setCustomValidity?.(""));
}

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const response = await fetch(form.action, {
    method: "POST",
    headers: { Accept: "application/json" },
    body: new FormData(form),
  });
  const result = await response.json();
  if (result.ok) {
    form.replaceWith("Thanks. We'll confirm the date by email.");
    return;
  }
  const fields = result.error.fields ?? {};
  for (const [name, messages] of Object.entries(fields)) {
    form.elements.namedItem(name)?.setCustomValidity?.(messages[0]);
  }
  if (!form.reportValidity()) return;
  alert(result.error.message);
});
```

- `input` でカスタムメッセージをクリアしてください。カスタムエラーのあるフィールドは空文字列に戻すまで不正なままで、そうしないと送信がまったくできなくなります。
- ブラウザーのチェックは `submit` イベントの前に実行されるので、サーバーに届くのはそれを通過した送信だけです。
- `429 rate_limited` のようにエラーに `fields` がない場合、`reportValidity()` は `true` を返し、スクリプトは全体向けのメッセージにフォールバックします。

## 送信前にサーバーのルールをチェックする

メールドメインのリストのようなサーバーだけのルールをリクエスト前に捕まえるには、フォームの公開定義を読み込んで [SDK](https://ikiform.com/ja/docs/sdk.md) の `validate` を実行します。同じフィールドのルールとメッセージをブラウザーで適用します:

`workshop.js`:

```js
import { getSchema, validate } from "@ikiform/sdk";

const form = document.querySelector("#workshop");
const result = await getSchema("frm_xxxxxxxxxxxxxxxx");
const fields = result.ok ? result.schema.fields : [];

form.addEventListener("submit", (event) => {
  const errors = validate(fields, new FormData(form));
  for (const control of form.elements) control.setCustomValidity?.(errors[control.name] ?? "");
  if (!form.reportValidity()) event.preventDefault();
});
```

> これは利便性のためのものと考えてください。正しさの基準はあくまでサーバーです。`getSchema` が公開するのはフィールド定義だけで、それはもともと公開情報です。誰でも `/f/<formId>/schema` から読めます。

## 動作を確認する

1. フォームの **Allowed domains** に自分のサイトのドメインを追加し、開発中は `localhost` も追加します。空のままにすると、どこからの送信も受け付けます。
2. 空のフォームを送信します。ブラウザーが **Your name** で止め、Network パネルには何も表示されません。
3. 開発者ツールでチーム人数の `max` を削除し、`25` を入力して送信します。プレーンな送信では Ikiform のエラーページが表示され、スクリプト版ではフィールドに **Must be at most 20** が表示されます。
4. ブラウザーを通さず curl で送信します: `curl -H "Accept: application/json" -H "Origin: https://example.com" -d team_size=abc https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx`。origin には自分の許可ドメインを指定します。`team_size` と、欠けているすべての必須フィールドについてのメッセージ付きで `422` が返ります。
5. ダッシュボードで **Submissions** を開きます。送ったすべてのフィールドを含む新しい送信データがあります。

## ハマりどころ

| 問題 | 原因と対処 |
| --- | --- |
| サーバーのエラーのあと、フォームが一切送信されない | カスタムの validity メッセージがクリアされていません。`input` で `setCustomValidity("")` を呼んでリセットしてください |
| 事前入力された値で `minlength` が働かない | `tooShort` は人がフィールドを編集したあとにだけ適用されます。サーバーのルールが捕まえます |
| `pattern` がまったく無視される | `v` フラグでは不正なパターンで、ブラウザーは不正なパターンを丸ごとスキップします。クラス内の `-` などの特殊文字をエスケープしてください |
| 入力前からエラーが表示される | CSS が `:invalid` を使っています。`:user-invalid` に切り替えてください |
| `422 validation_failed` | 送信がフォームのフィールド定義に合っていません。`error.fields` に各フィールドと理由が示されます。入力欄の `name` をフィールド ID に合わせてください。 |

送信の流れ全体は[概要](https://ikiform.com/ja/guides/html-form-submission.md#validation)にあり、送信に返りうるすべてのコードは[エラー](https://ikiform.com/ja/docs/errors.md)に一覧があります。

## よくある質問

### HTML フォームのバリデーションだけで十分ですか？

いいえ。それが動くのはあなたのページを開いたブラウザーの中だけで、ボットやスクリプトはエンドポイントに直接送信します。サーバーでも検証してください。Ikiform ではフォームのフィールドを定義すれば、すべての送信がチェックされます。

### サーバーのエラーメッセージを入力欄に表示するには？

input.setCustomValidity(message) を呼んでから form.reportValidity() を呼ぶと、ブラウザーのいつもの吹き出しにメッセージが表示されます。人がフィールドを編集したら setCustomValidity("") でクリアしてください。

### :invalid と :user-invalid の違いは何ですか？

:invalid は、ページ読み込み時の空の必須フィールドも含め、制約を満たさないすべてのフィールドにマッチします。:user-invalid は、人がフィールドを操作したか送信しようとしたあとにだけマッチします。

### フィールドがバリデーションに失敗すると Ikiform は何を返しますか？

何も保存せずに 422 validation_failed を返します。JSON のリクエストには、各フィールド ID をそのメッセージに対応付ける error.fields が返ります。プレーンなフォーム送信には、フィールドのラベルごとに一覧にしたホストされたエラーページが返ります。

## その他のガイド

- [HTML フォームの送信](https://ikiform.com/ja/guides/html-form-submission.md): フォームが送信されたときにブラウザーが行うこと。どのフィールドを送り、どうエンコードし、サーバーは何を返すべきか、そして Ikiform がどう応答するかを解説します。
- [ページを再読み込みせずに](https://ikiform.com/ja/guides/html-form-submission/without-page-reload.md): fetch と FormData でフォームを送り、訪問者をページにとどめる。フィールドのエラー、読み込み状態、リトライ、スクリプト失敗時のフォールバック付き。
- [action 属性](https://ikiform.com/ja/guides/html-form-submission/action-attribute.md): フォームの action 属性の役割、相対値や空の値の解決のされ方、formaction で 1 つのフォームを 2 か所に送る方法、静的ホストが 405 を返す理由を解説します。
- [送信後のリダイレクト](https://ikiform.com/ja/guides/html-form-submission/redirect-after-submit.md): フォーム送信後に訪問者を自分のサンクスページへ送る方法。303 リダイレクト、_redirect フィールド、許可されるドメイン、再読み込みの挙動を解説します。
