はじめに
サーバーサイドレンダリング+フォーム送信の構成を保守していると、こんなコードに出くわすことがあります。😇
<!-- チェックボックスはもうない。でも input[type="hidden"] だけ残っている -->
<input type="hidden" name="notifyCheck" value="true" />
// サーバー側のバリデーション
if (!form.isNotifyCheck()) {
errors.put("notifyCheck", "再通知フラグを確認してください");
}
「再通知フラグのチェックボックス? 画面にないけど?」
調べてみると、かつてはチェックボックスが存在していた。機能廃止の際にUIからは消したが、サーバー側を触りたくなかったため <input type="hidden"> に固定値 true を入れて代替した——という経緯が見えてきました。
結果として、サーバー側のバリデーションはfalse にならず、エラーパスは誰も踏めない dead code になっていました。
この記事で扱う「hidden フィールド」は <input type="hidden"> を指します。HTML5 の hidden 属性(要素の表示を消す属性)とは別物なので、混同しないようにしてください。
先にまとめ
| 確認事項 | 結論 |
|---|---|
display:none 内の input は submit されるか |
される(disabled でない要素は送信対象) |
<input type="hidden"> + 固定値パターンの問題 |
dead code 化だけでなく、仕様乖離・クライアント値信頼など複数の問題が複合する |
| 送信自体を止めたいとき |
disabled 属性を付与する、または DOM から取り除く |
| 送信されても識別しやすくしたいとき | フィールド名を分離し、route パラメータでルートを確定させてから検証する |
display:none は送信を止めない
HTML 仕様の定義
フォーム送信では、name を持ち、送信対象のフォーム部品であり、disabled でない要素が基本的に収集対象になります。display:none はスタイルの話であり、要素を disabled 状態にはしません。
<form method="post">
<!-- display:none でも name 属性があれば送信される -->
<div style="display:none">
<input type="text" name="hidden_field" value="送られます" />
</div>
<button type="submit">送信</button>
</form>
このフォームをサブミットすると、hidden_field=送られます はリクエストボディに含まれます。
disabled との違い
送信を止めたいなら disabled 属性が必要です。
<!-- これは送信されない -->
<input type="text" name="skip_me" value="値" disabled />
<!-- これは送信される(display:none は関係ない) -->
<input type="text" name="send_me" value="値" style="display:none" />
display:none と disabled は全く別の概念ですが、「画面に見えていないなら送られない」という直感で混同されやすいポイントです。
visibility:hidden や height:0 も同様
visibility:hidden、opacity:0、height:0 のいずれも disabled ではないため、同じく送信対象です。CSS でどれだけ「見えなく」しても、フォーム送信の観点では意味がありません。
実際に送信される値は要素種別によって変わる
「送信対象になりうる」とはいえ、何が届くかは要素種別と入力状態に依存します。
| 要素 | 非表示状態での送信値 |
|---|---|
input[type="text"] |
未入力なら空文字 ""
|
input[type="checkbox"] |
checked でなければパラメータ自体が存在しない |
input[type="radio"] |
checked のものだけ送信(グループ全体が未選択ならパラメータなし) |
input[type="hidden"] |
常に value が送信される |
select |
選択中の option の value
|
「非表示 = 空文字が届く」と一律に覚えるより、「disabled でない限り送信対象になりうる、何が届くかは要素種別による」と理解する方が正確です。
<input type="hidden"> + 固定値が引き起こす問題
よくある経緯
機能廃止やUI刷新の際、「サーバー側は変えたくない」という判断で次のようなパターンが生まれます。
Before(機能廃止前):
<label>
<input type="checkbox" name="notifyCheck" value="true" />
再通知を希望する
</label>
After(機能廃止後):
<!-- チェックボックスを削除し、type="hidden" で固定値を送り続ける -->
<input type="hidden" name="notifyCheck" value="true" />
サーバー側は変更なし:
if (!form.isNotifyCheck()) {
// ← 通常の画面操作では到達しない
errors.put("notifyCheck", "再通知フラグを確認してください");
}
何が問題なのか
1. エラーパスが dead code 化する
type="hidden" は常に value="true" を送るため、通常の画面操作では isNotifyCheck() が false になることはありません。結果として if ブロックの中身は実運用上ほぼ到達不能になり、UIテストや結合テストでも見落とされやすい dead code になります。
2. 画面仕様とサーバー仕様が乖離する
仕様書や画面設計に存在しないフィールドが、リクエストには含まれている状態になります。新しくプロジェクトに入ったメンバーが「画面にないのにサーバーに届いている値があるが、これは正常なのか異常なのか」と判断できなくなります。
3. コードから意図が読めなくなる
notifyCheck という名前と「再通知フラグを確認してください」というエラーメッセージは、元々の機能の文脈がなければ何のことか分かりません。保守担当者は「このフィールドは何のためにある?消していいのか?」で詰まります。
4. クライアント送信値を業務ロジックの前提にしてしまう
<input type="hidden"> の値はブラウザの開発者ツールから書き換えられます。「クライアントが true を送ってきたから通過させる」という設計は、本来サーバー側で持つべき業務ルールの判断を、制御できない場所に委ねていることになります。
5. 連鎖的に広がる
同じフォームを使う別画面にも type="hidden" フィールドを追加する必要が出てきます。同じパターンが複数ファイルに散在し、どこが「正」か分からなくなっていきます。
ラジオボタン切り替えで顕在化する別の問題
display:none ベースのラジオ切り替えでも、同じ構造的問題が起きます。
display:none 切り替えの場合
<form method="post">
<label><input type="radio" name="route" value="A" /> 電話番号で申し込む</label>
<label><input type="radio" name="route" value="B" /> メールで申し込む</label>
<div id="block-a">
<input type="text" name="phone_number" placeholder="電話番号" />
</div>
<div id="block-b" style="display:none">
<input type="text" name="email_address" placeholder="メールアドレス" />
</div>
</form>
ルートA を選択中でも、block-b が DOM に存在する限り email_address は送信対象です(text 入力は未入力なら空文字で届く)。ルートBを選べば phone_number も届きます。
サーバーは route パラメータで選択ルートを把握しつつ、届いた値の整合性を自前でチェックする必要が生まれます。条件分岐が増え、テストケースも増えていきます。
送信を制御する2つのアプローチ
「非表示のフィールドが送られる問題」への対処は、目的によって2通りに分かれます。
アプローチ1:送信自体を止める
disabled を動的に付与する
非表示にするタイミングで disabled 属性を付与すれば、その要素は送信対象から外れます。
function switchRoute(selected) {
document.querySelectorAll('[data-route]').forEach(block => {
const isTarget = block.dataset.route === selected;
block.style.display = isTarget ? '' : 'none';
// 表示と disabled を連動させる
block.querySelectorAll('input, select, textarea').forEach(el => {
el.disabled = !isTarget;
});
});
}
HTML の表示制御と disabled の制御を連動させるのがポイントです。
なお、disabled にした要素はサーバーに届かないため、「再表示したときに入力済みの値を復元したい」ケースでは別途 state 管理が必要になります。
DOM から取り除く
非選択ブロックを DOM ごと削除しても送信対象から外れます。ただし再選択時の再描画処理が必要になるため、静的な JSP/Thymeleaf 環境では管理コストが上がります。
アプローチ2:送信されても識別しやすくする
disabled 管理が複雑になるケースでは、フィールド名にルートを含めることでサーバー側の識別を容易にする方法があります。
<div data-route="A">
<input type="text" name="route_a_phone" placeholder="電話番号" />
</div>
<div data-route="B" style="display:none">
<input type="text" name="route_b_email" placeholder="メールアドレス" />
</div>
サーバー側のルート判定は、route パラメータを主キーにします。フィールドの有無でルートを推定するのは、判定が不安定になるため避けてください。
// ルート判定の主キーは route パラメータ
String route = request.getParameter("route");
if ("A".equals(route)) {
// ルートA 専用のバリデーション(route_a_phone だけを検証)
String phone = request.getParameter("route_a_phone");
// ...
} else if ("B".equals(route)) {
// ルートB 専用のバリデーション(route_b_email だけを検証)
String email = request.getParameter("route_b_email");
// ...
}
// 非該当ルートのパラメータは無視
route_b_email が空文字で届いていても、ルートAの分岐には入らないため問題になりません。
視覚制御と送信制御は別問題
height アニメーションやアコーディオンで開閉するUIでも、DOM にはどちらのブロックも存在し続けます。視覚的な開閉とフォーム送信の制御は、別の問題として切り離して考える必要があります。
「どちらのアプローチを選ぶか」の判断基準:
| 方式 | 向いている状況 |
|---|---|
| disabled 連動 | シンプルなラジオ切り替えで、送信自体を確実に止めたい |
| フィールド名分離 + route 判定 | disabled 管理が複雑、またはサーバー側で柔軟な処理が必要 |
4方式の比較
| 方式 | 非表示側の送信 | サーバー側の判定コスト | 将来の変更しやすさ |
|---|---|---|---|
| display:none のみ | される | 高(条件分岐増加) | 低 |
<input type="hidden"> + 固定値 |
される(常に同じ値) | バリデーションが死ぬ | 低(dead code 化) |
| disabled 連動 | されない | 低(disabled が保証) | 高 |
| フィールド名分離 + route 判定 | される場合があるが識別可 | 低(route で分岐) | 高 |
SPA・モダン環境では論点が変わる
React や Vue などの SPA 環境では、フォームの送信値はコンポーネントの state で管理され、送信時に state から JSON を組み立てます。「DOM の form submit 挙動起因」で非表示フィールドが混入する問題は発生しにくくなります。
ただし、SPA でも同種の問題は別の形で出てきます。
- state に非表示項目の値が残り続ける
-
display:noneのままコンポーネントを unmount せずに隠す - フォームライブラリが非表示フィールドの値を保持したまま送信する
違いは「DOM の form submit が原因」ではなく、「state の保持・破棄設計や JSON シリアライズの設計が原因」になる点です。「表示されていない項目の値を送信先に渡すべきか」という問いは、環境を問わず共通して意識する必要があります。
本記事の内容は、JSP・Thymeleaf・FreeMarker のようなサーバーサイドテンプレート+jQuery 環境で、この問題がブラウザの標準フォーム送信の挙動として直接露出するという話です。
まとめ
-
display:noneは見た目を変えるだけで、input の送信は止まらない。送信を止めるにはdisabledを付与するか、DOM から取り除く -
<input type="hidden">に固定値を入れてUIを「なかったこと」にすると、dead code 化・仕様乖離・クライアント値信頼の問題が複合的に起きる - ラジオボタン切り替えでは
routeパラメータでルートを確定してからフィールドを検証する。フィールドの有無でルートを推定しない - 視覚的な開閉とフォーム送信の制御は別問題。disabled 連動かフィールド名分離か、目的に応じて選ぶ
最後に、GMOコネクトではサービス開発支援や技術支援をはじめ、
幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。