入力チェック仕様とは、画面やAPIが受け取る値について「どの項目に・どの条件を・どの順番で・どの層で確かめ、合わないときにどのメッセージを出すか」を表にまとめた設計書です。 1項目を1行にして、型・必須・桁数・文字種・範囲・形式・相関条件・エラーメッセージID・実施層を列に並べると、実装する人とテストする人が同じ表を見て作業できます。

この記事では、そのまま貼って使える仕様書テンプレートと記入例、単項目チェック20種・相関チェック10種の一覧、各チェックの正規表現と境界値テストケースを載せます。正規表現とテストケースはすべて Python で動かし、出力をそのまま掲載しています。フレームワークごとの実装方法には触れず、仕様の書き方に絞ります。

確認環境:Ubuntu 24.04 / Python 3.11.15(標準ライブラリの re・unicodedata・datetime・sqlite3 のみ使用)

入力チェック仕様の早見表

決めること 書く内容 書き方の例
対象 画面ID・項目名・物理名 R-010 予約登録/利用開始日/start_date
型 文字列・整数・小数・日付・時刻・コード 日付
必須 必須・任意・条件付き必須 必須/任意/条件付き(C03)
桁数 最小〜最大。文字数かバイト数かを明記 1〜50文字
文字種 許可する文字の集合 半角英大文字・数字・ハイフン
範囲 最小値・最大値、境界を含むかどうか 1以上99以下
形式 書式または正規表現 YYYY-MM-DD、実在する日付
相関条件 ほかの項目との関係 利用開始日 ≦ 利用終了日
エラーメッセージID 文言の一覧と対応づけるID E-REL-001
実施層 画面・サーバー・DB制約のどこで行うか 画面:○、サーバー:○、DB:CHECK
実行順 単項目 → 相関 → DB照合 前の段でエラーなら後ろの段は行わない

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「入力チェック仕様の書き方 単項目・相関チェック一覧とテンプレート」

入力チェックの3つの段と用語

入力チェックは、見る範囲の広さで3つの段に分けて書くと整理しやすくなります。

段 見る範囲 例 仕様書での書き場所
単項目チェック 1つの項目の値だけ 必須、桁数、文字種、形式、範囲 項目ごとの行(型〜形式の列)
相関チェック 同じ入力の中の複数項目 日付の前後、条件付き必須、合計一致 相関条件の列+相関チェック一覧
DB照合チェック 保存済みのデータ マスタに存在するか、重複がないか、予約が重なっていないか DB照合チェック一覧

単項目チェックは「その値だけを見て判定できるもの」、相関チェックは「同じ入力のほかの値がないと判定できないもの」、DB照合は「保存済みのデータを読まないと判定できないもの」 と区切ると、どの表に書くかで迷いません。

入力チェック仕様書のテンプレート

項目別チェック仕様(1項目1行)

列は次の12個です。空欄にするときは「―」を入れ、書き漏れと区別できるようにします。

列 書く内容 書き方のきまり
No 画面内の連番 画面の表示順に振る
項目名 画面に表示するラベル メッセージに埋め込む名前と同じにする
物理名 リクエストの項目名 APIのキー名、フォームの name と一致させる
型 文字列/整数/小数/日付/時刻/コード DBの型ではなく入力値としての型
必須 必須/任意/条件付き 条件付きは相関チェックIDを書く
桁数 最小〜最大 単位(文字・バイト)を表の注記に書く
文字種 許可する文字 「半角数字」「全角カタカナ」など一覧の呼び名で統一
範囲・選択肢 最小値〜最大値、またはコード値 「以上・以下」で境界を含むことを示す
形式 書式、正規表現ID 単項目チェック一覧のID(S15 など)を書く
相関条件 相関チェックID C01 など。内容は相関チェック一覧に書く
エラーメッセージID 単項目チェックで出すメッセージ 複数あるときはチェックの順に並べる
実施層 画面/サーバー/DB 行う層に○、DBは制約名を書く

記入例:備品予約システムの予約登録画面(R-010)

架空の社内備品予約システムを題材にした記入例です。桁数の単位は文字数、前後の空白はチェックの前に取り除く前提で書いています。

No 項目名 物理名 型 必須 桁数 文字種 範囲・選択肢 形式 相関条件 エラーメッセージID 実施層(画面/サーバー/DB)
1 備品コード item_code 文字列 必須 6固定 半角英大文字・数字・ハイフン ― 英大文字2桁-数字3桁 ― E-REQ-001, E-FMT-003 ○/○/外部キー
2 利用開始日 start_date 日付 必須 10固定 半角数字・ハイフン 当日以降 S15 C01, C02 E-REQ-001, E-FMT-001 ○/○/NOT NULL
3 利用終了日 end_date 日付 必須 10固定 半角数字・ハイフン ― S15 C01, C02 E-REQ-001, E-FMT-001 ○/○/CHECK
4 数量 qty 整数 必須 1〜2 半角数字 1以上99以下 S12, S14 C08 E-REQ-001, E-FMT-002, E-RNG-001 ○/○/CHECK
5 利用目的 purpose コード 必須 ― ― MEETING / EVENT / OTHER S10 C03 E-REQ-001, E-COD-001 ○/○/CHECK
6 備考 note 文字列 条件付き(C03) 0〜200 制御文字以外 ― S11 C03 E-LEN-001, E-CHR-001 ○/○/―
7 申請者氏名カナ applicant_kana 文字列 必須 1〜40 全角カタカナ ― S09 ― E-REQ-001, E-LEN-001, E-CHR-002 ○/○/NOT NULL
8 連絡先メール email 文字列 条件付き(C05) 6〜254 半角英数字・記号 ― S17 C05, C06 E-LEN-001, E-FMT-004 ○/○/―
9 連絡先メール(確認) email_confirm 文字列 条件付き(C05) 6〜254 半角英数字・記号 ― S17 C06 E-FMT-004 ○/○/保存しない
10 連絡先電話番号 phone 文字列 条件付き(C05) 12〜13 半角数字・ハイフン ― S18 C05 E-FMT-005 ○/○/―

表計算ソフトに貼れるタブ区切りです。1行目が見出し、2行目以降が記入例です。

No  項目名 物理名 型   必須  桁数  文字種 範囲・選択肢  形式  相関条件    エラーメッセージID  画面  サーバー    DB
1   備品コード   item_code   文字列 必須  6固定 半角英大文字・数字・ハイフン  ―   英大文字2桁-数字3桁 ―   E-REQ-001, E-FMT-003    ○   ○   外部キー
2   利用開始日   start_date  日付  必須  10固定    半角数字・ハイフン   当日以降    S15 C01, C02    E-REQ-001, E-FMT-001    ○   ○   NOT NULL
3   利用終了日   end_date    日付  必須  10固定    半角数字・ハイフン   ―   S15 C01, C02    E-REQ-001, E-FMT-001    ○   ○   CHECK
4   数量  qty 整数  必須  1〜2 半角数字    1以上99以下 S12, S14    C08 E-REQ-001, E-FMT-002, E-RNG-001 ○   ○   CHECK
5   利用目的    purpose コード 必須  ―   ―   MEETING / EVENT / OTHER S10 C03 E-REQ-001, E-COD-001    ○   ○   CHECK
6   備考  note    文字列 条件付き(C03)   0〜200   制御文字以外  ―   S11 C03 E-LEN-001, E-CHR-001    ○   ○   ―

相関チェック仕様とDB照合チェック仕様

相関チェックとDB照合チェックは、項目別の表とは別に「1チェック1行」で書きます。エラーをどの項目に表示するか(表示先)を必ず決めておく と、画面の実装とテストの期待結果がそろいます。

ID 段 対象項目 条件(合格となる条件) 表示先 エラーメッセージID
C01 相関 利用開始日, 利用終了日 利用開始日 ≦ 利用終了日 利用終了日 E-REL-001
C02 相関 利用開始日, 利用終了日 開始日と終了日を含めて14日以内 利用終了日 E-REL-002
C03 相関 利用目的, 備考 利用目的が OTHER のとき備考が空でない 備考 E-REL-003
C05 相関 連絡先メール, 連絡先電話番号 どちらか一方が入力されている 連絡先メール E-REL-005
C06 相関 連絡先メール, 連絡先メール(確認) 2つの値が完全に一致する 連絡先メール(確認) E-REL-006
D01 DB照合 備品コード 備品マスタに存在する 備品コード E-DB-001
D02 DB照合 備品コード, 利用開始日, 利用終了日 同じ備品の予約と期間が重ならない 利用開始日 E-DB-002
ID  段   対象項目    条件(合格となる条件) 表示先 エラーメッセージID
C01 相関  利用開始日, 利用終了日    利用開始日 ≦ 利用終了日   利用終了日   E-REL-001
C02 相関  利用開始日, 利用終了日    開始日と終了日を含めて14日以内    利用終了日   E-REL-002
C03 相関  利用目的, 備考    利用目的が OTHER のとき備考が空でない  備考  E-REL-003
D01 DB照合    備品コード   備品マスタに存在する  備品コード   E-DB-001
D02 DB照合    備品コード, 利用開始日, 利用終了日 同じ備品の予約と期間が重ならない    利用開始日   E-DB-002

エラーメッセージ一覧

メッセージの文言は仕様書の各行に直接書かず、IDで参照します。同じ文言を複数の画面で使い回せて、文言の見直しも1か所で済みます。{0} には項目名、{1} 以降には桁数や範囲の値を入れます。

メッセージID 文言 使うチェック
E-REQ-001 {0}を入力してください。 必須
E-LEN-001 {0}は{1}文字以上{2}文字以内で入力してください。 最小・最大文字数
E-CHR-001 {0}に使用できない文字が含まれています。 制御文字なし
E-CHR-002 {0}は全角カタカナで入力してください。 文字種
E-FMT-001 {0}は YYYY-MM-DD 形式の実在する日付で入力してください。 日付
E-FMT-002 {0}は整数で入力してください。 整数形式
E-FMT-003 {0}は「英大文字2桁-数字3桁」の形式で入力してください。 個別の形式
E-FMT-004 {0}の形式が正しくありません。 メールアドレス
E-FMT-005 {0}はハイフン区切りで入力してください。 電話番号
E-RNG-001 {0}は{1}以上{2}以下で入力してください。 数値範囲
E-COD-001 {0}の選択肢が正しくありません。 コード値
E-REL-001 {0}は{1}以降の日付を入力してください。 日付の前後
E-REL-002 利用期間は{0}日以内で入力してください。 期間の上限
E-REL-003 {0}が「その他」のときは{1}を入力してください。 条件付き必須
E-DB-001 指定された{0}は登録されていません。 存在チェック
E-DB-002 指定された期間はすでに予約されています。 期間の重なり

IDは「種別3文字+連番」にしておくと、IDを見ただけでどの種類のチェックかがわかります(REQ=必須、LEN=桁数、CHR=文字種、FMT=形式、RNG=範囲、COD=コード値、REL=相関、DB=DB照合)。

単項目チェック20種の一覧(正規表現と境界値テストケース)

仕様書の「形式」列からIDで参照できるよう、単項目チェックを20種にまとめました。正規表現は文字列全体に一致するかどうか(Python の re.fullmatch)で判定する前提です。境界値テストケースは、後ろのスクリプトで実際に判定した値です。

ID チェック 条件・正規表現 境界値テストケース(合格) 境界値テストケース(エラー)
S01 必須 空白を除いて1文字以上 a 空文字、半角空白のみ、全角空白のみ、値なし
S02 最大文字数(20) 文字数 ≦ 20 19文字、20文字、空文字 21文字
S03 最小文字数(8) 文字数 ≧ 8 8文字、9文字 7文字
S04 固定文字数(6) 文字数 = 6 6文字 5文字、7文字
S05 最大バイト数(30) UTF-8のバイト数 ≦ 30 全角10文字(30バイト)、半角30文字 全角10文字+半角1文字、半角31文字
S06 半角数字 [0-9]+ 0、0123 123(全角)、12a、-1、末尾に改行
S07 半角英数字 [0-9A-Za-z]+ Abc123 abc-1、abc(全角)、abc 1
S08 パスワード文字種 (?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[!-~]{8,64} Abcdef12、Abcdef1!、64文字 7文字、英小文字なし、英大文字なし、数字なし、全角を含む、65文字
S09 全角カタカナ [ァ-ヶー]+ ヤマダ、サトー、ヴァ やまだ、半角カナ、空白を含む
S10 コード値 許可した値の一覧に含まれる PC pc(小文字)、DESK、空文字
S11 制御文字なし Unicodeの制御文字(カテゴリCc)を含まない abc、あ い タブ、改行、NUL文字を含む
S12 整数形式 -?(0|[1-9][0-9]*) 0、-5、12、-0 012、1.0、+5、空文字
S13 小数形式(整数5桁・小数2桁) (0|[1-9][0-9]{0,4})(\.[0-9]{1,2})? 0、99999、1.5、1.55 100000、1.555、.5、1.
S14 数値範囲(1〜99) 整数形式かつ 1 ≦ 値 ≦ 99 1、2、98、99 0、100
S15 日付(実在日) [0-9]{4}-[0-9]{2}-[0-9]{2} かつ暦に存在する 2026-02-28、2028-02-29 2026-02-29、2026-13-01、2026-04-31、2026/10/05
S16 時刻(HH:MM) ([01][0-9]|2[0-3]):[0-5][0-9] 00:00、23:59 24:00、12:60、9:00
S17 メールアドレス(簡易) 254文字以内かつ [0-9A-Za-z._%+-]+@[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)+ taro@example.com taro@example、@ が2つ、空白を含む、@ の前が空
S18 電話番号(ハイフンあり) 0[0-9]{1,4}-[0-9]{1,4}-[0-9]{4} かつ数字が10桁か11桁 03-1234-5678、090-1234-5678 ハイフンなし、数字9桁、先頭が0以外
S19 郵便番号 [0-9]{3}-[0-9]{4} 100-0001 1000001、100-001、全角数字
S20 URL(http/https) https?://[0-9A-Za-z.-]+(:[0-9]{1,5})?(/[!-~]*)? https://example.com、ポートとクエリつき ftp://、ホストなし、空白を含む

S12・S14 の -0 や S15 の「当日以降」のように、形式だけでは決まらない扱いは「範囲」の列で決めます。S17 のメールアドレスは、形式を厳密に追うほど正規表現が長くなるため、仕様としては「簡易な形式チェック+確認メールの到達で確かめる」と書く のが扱いやすい方法です。

20種を検算するPythonスクリプト

import re
import unicodedata
from datetime import date

# --- 単項目チェック20種(True なら合格)---
def s01_required(v):      return v is not None and v.strip() != ""
def s02_max_len(v, n=20): return len(v) <= n
def s03_min_len(v, n=8):  return len(v) >= n
def s04_fixed_len(v, n=6): return len(v) == n
def s05_max_bytes(v, n=30): return len(v.encode("utf-8")) <= n
def s06_digits(v):        return re.fullmatch(r"[0-9]+", v) is not None
def s07_alnum(v):         return re.fullmatch(r"[0-9A-Za-z]+", v) is not None
def s08_password(v):      return re.fullmatch(r"(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[!-~]{8,64}", v) is not None
def s09_katakana(v):      return re.fullmatch(r"[ァ-ヶー]+", v) is not None
def s10_code(v, allowed=("PC", "MONITOR", "ROOM")): return v in allowed
def s11_no_control(v):    return not any(unicodedata.category(c) == "Cc" for c in v)
def s12_integer(v):       return re.fullmatch(r"-?(0|[1-9][0-9]*)", v) is not None
def s13_decimal(v):       return re.fullmatch(r"(0|[1-9][0-9]{0,4})(\.[0-9]{1,2})?", v) is not None
def s14_range(v, lo=1, hi=99): return s12_integer(v) and lo <= int(v) <= hi
def s15_date(v):
    if re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}", v) is None:
        return False
    try:
        date.fromisoformat(v)
        return True
    except ValueError:
        return False
def s16_time(v):          return re.fullmatch(r"([01][0-9]|2[0-3]):[0-5][0-9]", v) is not None
def s17_email(v):         return len(v) <= 254 and re.fullmatch(r"[0-9A-Za-z._%+-]+@[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)+", v) is not None
def s18_phone(v):         return re.fullmatch(r"0[0-9]{1,4}-[0-9]{1,4}-[0-9]{4}", v) is not None and len(v.replace("-", "")) in (10, 11)
def s19_postal(v):        return re.fullmatch(r"[0-9]{3}-[0-9]{4}", v) is not None
def s20_url(v):           return re.fullmatch(r"https?://[0-9A-Za-z.-]+(:[0-9]{1,5})?(/[!-~]*)?", v) is not None

# (ID, チェック名, 関数, [(入力, 期待), ...]) 期待: True=合格 / False=エラー
CASES = [
 ("S01", "必須", s01_required, [("a", True), ("", False), ("   ", False), (" ", False), (None, False)]),
 ("S02", "最大文字数20", s02_max_len, [("あ" * 19, True), ("あ" * 20, True), ("あ" * 21, False), ("", True)]),
 ("S03", "最小文字数8", s03_min_len, [("a" * 7, False), ("a" * 8, True), ("a" * 9, True)]),
 ("S04", "固定文字数6", s04_fixed_len, [("12345", False), ("123456", True), ("1234567", False)]),
 ("S05", "最大バイト数30", s05_max_bytes, [("あ" * 10, True), ("あ" * 10 + "a", False), ("a" * 30, True), ("a" * 31, False)]),
 ("S06", "半角数字", s06_digits, [("0", True), ("0123", True), ("123", False), ("12a", False), ("-1", False), ("123\n", False)]),
 ("S07", "半角英数字", s07_alnum, [("Abc123", True), ("abc-1", False), ("abc", False), ("abc 1", False)]),
 ("S08", "パスワード文字種", s08_password, [("Abcdef12", True), ("Abcde12", False), ("abcdef12", False), ("ABCDEF12", False), ("Abcdefgh", False), ("Abcdef1!", True), ("Abcdef1あ", False), ("A1" + "a" * 62, True), ("A1" + "a" * 63, False)]),
 ("S09", "全角カタカナ", s09_katakana, [("ヤマダ", True), ("サトー", True), ("ヴァ", True), ("やまだ", False), ("ヤマダ", False), ("ヤマダ タロウ", False)]),
 ("S10", "コード値", s10_code, [("PC", True), ("pc", False), ("DESK", False), ("", False)]),
 ("S11", "制御文字なし", s11_no_control, [("abc", True), ("a\tb", False), ("a\nb", False), ("a\x00b", False), ("あ い", True)]),
 ("S12", "整数形式", s12_integer, [("0", True), ("-5", True), ("12", True), ("012", False), ("1.0", False), ("+5", False), ("", False), ("-0", True)]),
 ("S13", "小数形式(整数5桁・小数2桁)", s13_decimal, [("0", True), ("99999", True), ("100000", False), ("1.5", True), ("1.55", True), ("1.555", False), (".5", False), ("1.", False)]),
 ("S14", "数値範囲1〜99", s14_range, [("0", False), ("1", True), ("2", True), ("98", True), ("99", True), ("100", False)]),
 ("S15", "日付(実在日)", s15_date, [("2026-10-05", True), ("2026-02-28", True), ("2026-02-29", False), ("2028-02-29", True), ("2026-13-01", False), ("2026-04-31", False), ("2026/10/05", False), ("20261005", False)]),
 ("S16", "時刻HH:MM", s16_time, [("00:00", True), ("23:59", True), ("24:00", False), ("12:60", False), ("9:00", False)]),
 ("S17", "メールアドレス(簡易)", s17_email, [("taro@example.com", True), ("taro.y+tag@mail.example.co.jp", True), ("taro@example", False), ("taro@@example.com", False), ("taro @example.com", False), ("@example.com", False)]),
 ("S18", "電話番号(ハイフンあり)", s18_phone, [("03-1234-5678", True), ("090-1234-5678", True), ("0120-123-4567", True), ("0312345678", False), ("03-1234-567", False), ("13-1234-5678", False)]),
 ("S19", "郵便番号", s19_postal, [("100-0001", True), ("1000001", False), ("100-001", False), ("100-0001", False)]),
 ("S20", "URL(http/https)", s20_url, [("https://example.com", True), ("http://example.com:8080/a?b=1", True), ("ftp://example.com", False), ("https://", False), ("https://example.com/a b", False)]),
]

def show(v):
    if v is None:
        return "None"
    if len(v) > 20:
        return f"'{v[:6]}…'({len(v)}文字)"
    return "'" + v.replace("\n", "\\n").replace("\t", "\\t").replace("\x00", "\\x00") + "'"

total = ng = 0
for cid, name, fn, cases in CASES:
    ok_list, err_list = [], []
    for value, expected in cases:
        actual = fn(value)
        total += 1
        if actual != expected:
            ng += 1
            print(f"不一致: {cid} {show(value)} 期待={expected} 結果={actual}")
        (ok_list if actual else err_list).append(show(value))
    print(f"[{cid}] {name}")
    print(f"   合格  : {' / '.join(ok_list)}")
    print(f"   エラー: {' / '.join(err_list)}")
print(f"\n{len(CASES)}種 {total}ケース / 期待と不一致 {ng}件")

実行結果です(python3 single_checks.py)。

[S01] 必須
   合格  : 'a'
   エラー: '' / '   ' / ' ' / None
[S02] 最大文字数20
   合格  : 'あああああああああああああああああああ' / 'ああああああああああああああああああああ' / ''
   エラー: 'ああああああ…'(21文字)
[S03] 最小文字数8
   合格  : 'aaaaaaaa' / 'aaaaaaaaa'
   エラー: 'aaaaaaa'
[S04] 固定文字数6
   合格  : '123456'
   エラー: '12345' / '1234567'
[S05] 最大バイト数30
   合格  : 'ああああああああああ' / 'aaaaaa…'(30文字)
   エラー: 'ああああああああああa' / 'aaaaaa…'(31文字)
[S06] 半角数字
   合格  : '0' / '0123'
   エラー: '123' / '12a' / '-1' / '123\n'
[S07] 半角英数字
   合格  : 'Abc123'
   エラー: 'abc-1' / 'abc' / 'abc 1'
[S08] パスワード文字種
   合格  : 'Abcdef12' / 'Abcdef1!' / 'A1aaaa…'(64文字)
   エラー: 'Abcde12' / 'abcdef12' / 'ABCDEF12' / 'Abcdefgh' / 'Abcdef1あ' / 'A1aaaa…'(65文字)
[S09] 全角カタカナ
   合格  : 'ヤマダ' / 'サトー' / 'ヴァ'
   エラー: 'やまだ' / 'ヤマダ' / 'ヤマダ タロウ'
[S10] コード値
   合格  : 'PC'
   エラー: 'pc' / 'DESK' / ''
[S11] 制御文字なし
   合格  : 'abc' / 'あ い'
   エラー: 'a\tb' / 'a\nb' / 'a\x00b'
[S12] 整数形式
   合格  : '0' / '-5' / '12' / '-0'
   エラー: '012' / '1.0' / '+5' / ''
[S13] 小数形式(整数5桁・小数2桁)
   合格  : '0' / '99999' / '1.5' / '1.55'
   エラー: '100000' / '1.555' / '.5' / '1.'
[S14] 数値範囲1〜99
   合格  : '1' / '2' / '98' / '99'
   エラー: '0' / '100'
[S15] 日付(実在日)
   合格  : '2026-10-05' / '2026-02-28' / '2028-02-29'
   エラー: '2026-02-29' / '2026-13-01' / '2026-04-31' / '2026/10/05' / '20261005'
[S16] 時刻HH:MM
   合格  : '00:00' / '23:59'
   エラー: '24:00' / '12:60' / '9:00'
[S17] メールアドレス(簡易)
   合格  : 'taro@example.com' / 'taro.y…'(29文字)
   エラー: 'taro@example' / 'taro@@example.com' / 'taro @example.com' / '@example.com'
[S18] 電話番号(ハイフンあり)
   合格  : '03-1234-5678' / '090-1234-5678' / '0120-123-4567'
   エラー: '0312345678' / '03-1234-567' / '13-1234-5678'
[S19] 郵便番号
   合格  : '100-0001'
   エラー: '1000001' / '100-001' / '100-0001'
[S20] URL(http/https)
   合格  : 'https://example.com' / 'http:/…'(29文字)
   エラー: 'ftp://example.com' / 'https://' / 'https:…'(23文字)

20種 109ケース / 期待と不一致 0件

正規表現を仕様に書くときの3つの注意

仕様書に正規表現を書くときは、「どの方法で照合するか」まで決めておくと実装のぶれがなくなります。次のスクリプトで違いを確かめます。

import re

print(bool(re.match(r"^[0-9]+$", "123\n")))      # 末尾の改行を通してしまう
print(bool(re.fullmatch(r"[0-9]+", "123\n")))    # fullmatch なら通さない
print(bool(re.fullmatch(r"\d+", "123")))        # \d は全角数字にも一致する
print(bool(re.fullmatch(r"[0-9]+", "123")))     # [0-9] は半角だけ
print(len("あいう"), len("あいう".encode("utf-8")))  # 文字数とバイト数
print(len("𠮷"), len("𠮷".encode("utf-8")), len("𠮷".encode("utf-16-le")) // 2)  # 文字数・UTF-8・UTF-16
True
False
True
False
3 9
1 4 2
注意 起きること 仕様書での書き方
末尾の改行 Python の re.match で ^[0-9]+$ を使うと、末尾に改行が付いた 123\n も一致する(1行目が True) 「文字列全体に一致」と明記し、re.fullmatch で照合する
\d の範囲 Python の \d は全角数字 123 にも一致する(3行目が True) 半角数字は [0-9] と書く
文字数とバイト数 「あいう」は3文字・UTF-8で9バイト。「𠮷」は1文字・UTF-8で4バイト・UTF-16では2単位 桁数の単位(文字数/バイト数)と、数える側(画面・サーバー・DB)を注記する

相関チェック10種の一覧

相関チェックは「合格となる条件」を1文で書き、境界になる組み合わせをテストケースにします。

ID チェック 合格となる条件 境界値テストケース(合格) 境界値テストケース(エラー)
C01 日付の前後 開始日 ≦ 終了日 終了日が翌日、同日 終了日が前日
C02 期間の上限 開始日と終了日を含めて14日以内 13日間、14日間 15日間
C03 条件付き必須 目的が OTHER なら備考が空でない OTHER+備考あり、MEETING+備考なし OTHER+備考なし
C04 条件付き入力不可 利用者区分が GUEST なら部署コードが空 GUEST+部署なし、STAFF+部署あり GUEST+部署あり
C05 いずれか必須 電話番号かメールのどちらかが入力されている 電話のみ、メールのみ 両方とも空
C06 一致 メールと確認用メールが完全に一致 同じ値 大文字と小文字が違う値
C07 不一致 新しいパスワードが現在のパスワードと異なる 異なる値 同じ値
C08 合計一致 明細の数量の合計 = 合計数量 2+3=5、明細0件で合計0 2+3 に対して合計6
C09 大小関係 最小数量 ≦ 最大数量 同じ値、最大が1大きい 最小が1大きい
C10 明細の重複なし 明細の中に同じ備品コードがない すべて異なるコード 同じコードが2行

10種を検算するPythonスクリプト

from datetime import date

D = date.fromisoformat

# --- 相関チェック10種(True なら合格)。入力は辞書で受け取る ---
def c01_date_order(f):    return D(f["start"]) <= D(f["end"])
def c02_max_period(f):    return (D(f["end"]) - D(f["start"])).days + 1 <= 14
def c03_required_if(f):   return f["purpose"] != "OTHER" or f["note"].strip() != ""
def c04_forbidden_if(f):  return f["user_type"] != "GUEST" or f["dept_code"] == ""
def c05_either(f):        return f["phone"] != "" or f["email"] != ""
def c06_same(f):          return f["email"] == f["email_confirm"]
def c07_different(f):     return f["new_password"] != f["current_password"]
def c08_sum(f):           return sum(d["qty"] for d in f["details"]) == f["total_qty"]
def c09_min_max(f):       return f["min_qty"] <= f["max_qty"]
def c10_no_duplicate(f):
    codes = [d["item_code"] for d in f["details"]]
    return len(codes) == len(set(codes))

CASES = [
 ("C01", "日付の前後(開始日<=終了日)", c01_date_order, [
    ({"start": "2026-10-05", "end": "2026-10-06"}, True),
    ({"start": "2026-10-05", "end": "2026-10-05"}, True),
    ({"start": "2026-10-05", "end": "2026-10-04"}, False)]),
 ("C02", "期間の上限(14日以内)", c02_max_period, [
    ({"start": "2026-10-05", "end": "2026-10-17"}, True),
    ({"start": "2026-10-05", "end": "2026-10-18"}, True),
    ({"start": "2026-10-05", "end": "2026-10-19"}, False)]),
 ("C03", "条件付き必須(目的=OTHERなら備考)", c03_required_if, [
    ({"purpose": "OTHER", "note": "展示会で使用"}, True),
    ({"purpose": "OTHER", "note": ""}, False),
    ({"purpose": "MEETING", "note": ""}, True)]),
 ("C04", "条件付き入力不可(GUESTは部署なし)", c04_forbidden_if, [
    ({"user_type": "GUEST", "dept_code": ""}, True),
    ({"user_type": "GUEST", "dept_code": "D01"}, False),
    ({"user_type": "STAFF", "dept_code": "D01"}, True)]),
 ("C05", "いずれか必須(電話かメール)", c05_either, [
    ({"phone": "03-1234-5678", "email": ""}, True),
    ({"phone": "", "email": "taro@example.com"}, True),
    ({"phone": "", "email": ""}, False)]),
 ("C06", "一致(メールと確認用)", c06_same, [
    ({"email": "taro@example.com", "email_confirm": "taro@example.com"}, True),
    ({"email": "taro@example.com", "email_confirm": "Taro@example.com"}, False)]),
 ("C07", "不一致(新旧パスワード)", c07_different, [
    ({"new_password": "Abcdef12", "current_password": "Xyzxyz99"}, True),
    ({"new_password": "Abcdef12", "current_password": "Abcdef12"}, False)]),
 ("C08", "合計一致(明細の数量=合計数量)", c08_sum, [
    ({"details": [{"qty": 2}, {"qty": 3}], "total_qty": 5}, True),
    ({"details": [{"qty": 2}, {"qty": 3}], "total_qty": 6}, False),
    ({"details": [], "total_qty": 0}, True)]),
 ("C09", "大小関係(最小数量<=最大数量)", c09_min_max, [
    ({"min_qty": 1, "max_qty": 1}, True),
    ({"min_qty": 1, "max_qty": 2}, True),
    ({"min_qty": 2, "max_qty": 1}, False)]),
 ("C10", "明細の重複なし(備品コード)", c10_no_duplicate, [
    ({"details": [{"item_code": "PC-001"}, {"item_code": "PC-002"}]}, True),
    ({"details": [{"item_code": "PC-001"}, {"item_code": "PC-001"}]}, False)]),
]

def brief(f):
    return ", ".join(f"{k}={v}" for k, v in f.items())

total = ng = 0
for cid, name, fn, cases in CASES:
    print(f"[{cid}] {name}")
    for form, expected in cases:
        actual = fn(form)
        total += 1
        ng += actual != expected
        print(f"   {'合格  ' if actual else 'エラー'} {brief(form)}")
print(f"\n{len(CASES)}種 {total}ケース / 期待と不一致 {ng}件")
[C01] 日付の前後(開始日<=終了日)
   合格   start=2026-10-05, end=2026-10-06
   合格   start=2026-10-05, end=2026-10-05
   エラー start=2026-10-05, end=2026-10-04
[C02] 期間の上限(14日以内)
   合格   start=2026-10-05, end=2026-10-17
   合格   start=2026-10-05, end=2026-10-18
   エラー start=2026-10-05, end=2026-10-19
[C03] 条件付き必須(目的=OTHERなら備考)
   合格   purpose=OTHER, note=展示会で使用
   エラー purpose=OTHER, note=
   合格   purpose=MEETING, note=
[C04] 条件付き入力不可(GUESTは部署なし)
   合格   user_type=GUEST, dept_code=
   エラー user_type=GUEST, dept_code=D01
   合格   user_type=STAFF, dept_code=D01
[C05] いずれか必須(電話かメール)
   合格   phone=03-1234-5678, email=
   合格   phone=, email=taro@example.com
   エラー phone=, email=
[C06] 一致(メールと確認用)
   合格   email=taro@example.com, email_confirm=taro@example.com
   エラー email=taro@example.com, email_confirm=Taro@example.com
[C07] 不一致(新旧パスワード)
   合格   new_password=Abcdef12, current_password=Xyzxyz99
   エラー new_password=Abcdef12, current_password=Abcdef12
[C08] 合計一致(明細の数量=合計数量)
   合格   details=[{'qty': 2}, {'qty': 3}], total_qty=5
   エラー details=[{'qty': 2}, {'qty': 3}], total_qty=6
   合格   details=[], total_qty=0
[C09] 大小関係(最小数量<=最大数量)
   合格   min_qty=1, max_qty=1
   合格   min_qty=1, max_qty=2
   エラー min_qty=2, max_qty=1
[C10] 明細の重複なし(備品コード)
   合格   details=[{'item_code': 'PC-001'}, {'item_code': 'PC-002'}]
   エラー details=[{'item_code': 'PC-001'}, {'item_code': 'PC-001'}]

10種 27ケース / 期待と不一致 0件

C02 の「14日以内」は、両端の日を含めるかどうかで1日ずれます。上のスクリプトは「終了日−開始日+1」で数えているので、10月5日〜18日が14日間で合格、19日までが15日間でエラーです。仕様書には「開始日と終了日を含めて」のように数え方を書いておきます。

チェックの実行順:単項目→相関→DB照合

チェックは「単項目 → 相関 → DB照合」の順に行い、ある段でエラーが出たら後ろの段へは進まない と決めておくと、次の利点があります。

  • 相関チェックは、各項目が正しい型になっている前提で書ける(日付として読めない値どうしの前後は比べられない)
  • DBへの問い合わせは、形式が正しい入力のときだけ行えば済む
  • 同じ段の中のエラーはまとめて返せるので、利用者は一度に直せる
順 段 エラーの返し方 前提
0 前処理 ― 前後の空白の除去、全角・半角の変換などを行うなら、ここで行うと仕様に書く
1 単項目チェック 全項目を確かめ、エラーをまとめて返す。1項目の中では必須→桁数→文字種→形式→範囲の順で最初の1件 なし
2 相関チェック 全チェックを確かめ、まとめて返す 単項目がすべて合格
3 DB照合チェック 存在チェック→重複・重なりの順で最初の1件 相関がすべて合格

この順番を、備品予約の入力で動かして確かめます。DB照合には Python に付属する SQLite を使っています。

import re
import sqlite3
from datetime import date

db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE items (item_code TEXT PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE reservations (
  id INTEGER PRIMARY KEY, item_code TEXT NOT NULL REFERENCES items(item_code),
  start_date TEXT NOT NULL, end_date TEXT NOT NULL);
INSERT INTO items VALUES ('PC-001', 'ノートPC'), ('RM-001', '会議室A');
INSERT INTO reservations VALUES (1, 'PC-001', '2026-10-10', '2026-10-12');
""")

def is_date(v):
    try:
        date.fromisoformat(v)
        return re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}", v) is not None
    except ValueError:
        return False

def single(f):
    errors = []
    if f["item_code"] == "":
        errors.append(("E-REQ-001", "item_code"))
    elif re.fullmatch(r"[A-Z]{2}-[0-9]{3}", f["item_code"]) is None:
        errors.append(("E-FMT-003", "item_code"))
    for key in ("start_date", "end_date"):
        if f[key] == "":
            errors.append(("E-REQ-001", key))
        elif not is_date(f[key]):
            errors.append(("E-FMT-001", key))
    return errors

def cross(f):
    errors = []
    start, end = date.fromisoformat(f["start_date"]), date.fromisoformat(f["end_date"])
    if start > end:
        errors.append(("E-REL-001", "start_date,end_date"))
    elif (end - start).days + 1 > 14:
        errors.append(("E-REL-002", "start_date,end_date"))
    return errors

def db_check(f):
    if db.execute("SELECT 1 FROM items WHERE item_code = ?", (f["item_code"],)).fetchone() is None:
        return [("E-DB-001", "item_code")]
    overlap = db.execute(
        "SELECT COUNT(*) FROM reservations WHERE item_code = ? AND start_date <= ? AND end_date >= ?",
        (f["item_code"], f["end_date"], f["start_date"])).fetchone()[0]
    return [("E-DB-002", "start_date,end_date")] if overlap else []

def validate(f):
    for stage, fn in (("単項目", single), ("相関", cross), ("DB照合", db_check)):
        errors = fn(f)
        if errors:
            return stage, errors          # エラーが出た段で止め、後ろの段は実行しない
    return "すべて通過", []

inputs = [
    {"item_code": "", "start_date": "2026-02-30", "end_date": ""},
    {"item_code": "PC-001", "start_date": "2026-10-12", "end_date": "2026-10-10"},
    {"item_code": "PC-001", "start_date": "2026-10-01", "end_date": "2026-10-15"},
    {"item_code": "PC-999", "start_date": "2026-10-20", "end_date": "2026-10-21"},
    {"item_code": "PC-001", "start_date": "2026-10-12", "end_date": "2026-10-13"},
    {"item_code": "PC-001", "start_date": "2026-10-13", "end_date": "2026-10-14"},
]
for no, f in enumerate(inputs, 1):
    stage, errors = validate(f)
    print(f"{no}. {f['item_code'] or '(空)'} {f['start_date']}〜{f['end_date'] or '(空)'}")
    print(f"   止まった段: {stage} {errors if errors else ''}")
1. (空) 2026-02-30〜(空)
   止まった段: 単項目 [('E-REQ-001', 'item_code'), ('E-FMT-001', 'start_date'), ('E-REQ-001', 'end_date')]
2. PC-001 2026-10-12〜2026-10-10
   止まった段: 相関 [('E-REL-001', 'start_date,end_date')]
3. PC-001 2026-10-01〜2026-10-15
   止まった段: 相関 [('E-REL-002', 'start_date,end_date')]
4. PC-999 2026-10-20〜2026-10-21
   止まった段: DB照合 [('E-DB-001', 'item_code')]
5. PC-001 2026-10-12〜2026-10-13
   止まった段: DB照合 [('E-DB-002', 'start_date,end_date')]
6. PC-001 2026-10-13〜2026-10-14
   止まった段: すべて通過 

1番目の入力は、備品コードが空・開始日が実在しない日付・終了日が空の3件が単項目の段でまとめて返り、相関とDB照合には進んでいません。5番目は予約済みの期間(10月10日〜12日)と1日重なるためエラー、6番目は重ならないので通過しています。

期間の重なりは 「既存の開始日 ≦ 新しい終了日」かつ「既存の終了日 ≧ 新しい開始日」 の1つの条件で判定できます。仕様書にもこの形で書いておくと、境界(同じ日を重なりとみなすか)がはっきりします。

画面・サーバー・DB制約の分担表

同じチェックを複数の層で行うのは重複ではなく、役割が違います。画面は利用者にすぐ知らせるため、サーバーは受け取った値を必ず確かめるため、DB制約は保存されるデータの整合性を最後に守るため です。

チェック 画面 サーバー DB制約 補足
必須 ○ ○ NOT NULL 空文字と NULL の扱いをそろえる
桁数 ○ ○ 列の長さ 文字数とバイト数の単位をそろえる
文字種・形式 ○ ○ CHECK(必要な列だけ) 正規表現は画面とサーバーで同じものを使う
数値範囲 ○ ○ CHECK 範囲が変わりやすい値はサーバーだけにする
コード値 ○(選択式) ○ CHECK または外部キー 選択式の入力でもサーバーで確かめる
相関(日付の前後など) ○ ○ CHECK(同じ行の中で完結するもの) 複数行にまたがる条件はサーバーで行う
存在チェック ― ○ 外部キー 画面は候補から選ぶ形にすると入力が楽になる
一意・重複 ― ○ UNIQUE 同時に登録されたときは DB制約が最後に止める
期間の重なり ― ○ ― 同時登録への対策を別途決める

実施層を決めるときの判断基準は次のとおりです。

判断の質問 はい いいえ
入力した値だけで判定できるか 画面とサーバーの両方 サーバー(とDB制約)
条件が将来変わりやすいか サーバーだけに置く DB制約にも置く
複数の利用者が同時に登録すると結果が変わるか DB制約(UNIQUE・外部キー)を必ず置く サーバーのチェックで足りる
ほかのプログラム(バッチ・別のAPI)も同じ表に書き込むか DB制約を置く サーバーのチェックで足りる

入力チェック仕様のチェックリスト

レビューの前に、次の項目を確かめます。

No 確認すること
1 すべての入力項目が1行ずつ載っている(選択式・非表示の項目も含む)
2 空欄がなく、該当なしは「―」になっている
3 桁数の単位(文字数・バイト数)が書いてある
4 範囲は「以上・以下」「より大きい・未満」で境界がわかる
5 日数・期間は両端を含むかどうかが書いてある
6 正規表現は「文字列全体に一致」と明記してある
7 前処理(空白の除去、全角・半角の変換)の有無が書いてある
8 空文字・空白のみ・値なしの扱いが決まっている
9 相関チェックに表示先の項目が書いてある
10 条件付き必須は、条件を満たさないときに値が入っていた場合の扱いも決まっている
11 すべてのチェックにエラーメッセージIDがあり、メッセージ一覧に文言がある
12 実施層(画面・サーバー・DB)が全行に書いてある
13 実行順と、エラーが出たときに後ろの段へ進むかどうかが書いてある
14 各チェックに境界値のテストケース(合格とエラーの両方)がある

よくある質問

Q. 単項目チェックと相関チェックは、どこで線を引けばよいですか? A. その項目の値だけで判定できれば単項目、同じ入力のほかの項目が必要なら相関です。「利用開始日が当日以降」は現在の日付と比べますが、ほかの入力項目は使わないので単項目(範囲)に書きます。保存済みのデータを読む必要があるものは DB照合に分けます。

Q. 画面でチェックしていれば、サーバー側のチェックは省けますか? A. サーバー側では必ず行います。画面を通らない経路(APIの直接呼び出しなど)でも値が届くためです。画面のチェックは利用者にすぐ知らせるためのもの、と役割を分けて仕様書の実施層に両方を書きます。

Q. エラーは1件ずつ返すのと、まとめて返すのと、どちらがよいですか? A. 同じ段の中はまとめて返すと、利用者が一度に直せます。この記事の例では、単項目の段は全項目分をまとめ、1項目の中では最初の1件だけにしています。どちらにするかを仕様書の「実行順」に書いておくことが大切です。

Q. 正規表現は仕様書にそのまま書くべきですか? A. 一覧表に1か所だけ書き、各項目の行からはID(S06 など)で参照するのがおすすめです。正規表現を見直すときに直す場所が1か所で済み、読み手は「半角数字」のような呼び名で内容を把握できます。

まとめ

  • 入力チェック仕様は「1項目1行」の表 にし、型・必須・桁数・文字種・範囲・形式・相関条件・エラーメッセージID・実施層を列にする
  • 単項目20種・相関10種の一覧をIDつきで用意 し、項目の行からはIDで参照する
  • 実行順は単項目→相関→DB照合。前の段でエラーなら後ろの段へ進まないと決める
  • 画面は知らせる役、サーバーは必ず確かめる役、DB制約は整合性を守る役 として分担表に書く
  • 正規表現は「文字列全体に一致」で照合 し、半角数字は [0-9] と書く
  • 境界値テストケースは合格とエラーの両方 を仕様の段階で決め、日数は両端を含むかどうかを書く

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「入力チェック仕様の書き方 単項目・相関チェック一覧とテンプレート」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?