0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Zod + TypeScript で日本語フォーム入力を正規化する — 全角→半角、ひらがな→カタカナ自動変換

0
Posted at

はじめに

日本語のWebフォームでは、ユーザーが全角数字(123)で入力したり、ひらがな(かぶしきがいしゃ)でカタカナ欄に入力したりすることが日常的に起こります。

「半角数字で入力してください」とエラーを出すのは簡単ですが、ユーザー体験としては最悪です。

Zod の transform を使えば、バリデーション前に自動変換を挟めます。ユーザーは何も気にせず入力でき、システム側で正規化される。この実装パターンを紹介します。

課題: 日本語入力の表記揺れ

入力例 期待値 問題
1234 1234 全角数字
0005 0005 全角ゼロ
かぶしきがいしゃ カブシキガイシャ ひらがな→カタカナ
10,000 10000 全角数字 + カンマ
(株) (株) 全角括弧

これらを「入力エラー」にするのではなく、自動変換してから検証します。

正規化関数

全角英数字 → 半角変換

// lib/validators/normalize.ts

export function toHalfWidth(str: string): string {
    return str
        // 全角英数字(A-Z, a-z, 0-9) → 半角(A-Z, a-z, 0-9)
        .replace(/[A-Za-z0-9]/g, (s) =>
            String.fromCharCode(s.charCodeAt(0) - 0xFEE0)
        )
        // 全角スペース → 半角スペース
        .replace(/ /g, " ");
}

仕組み: Unicode上で全角英数字は半角英数字の 0xFEE0 先にあります。コードポイントを引くだけで変換できます。

"A" (U+FF21) - 0xFEE0 = "A" (U+0041)
"0" (U+FF10) - 0xFEE0 = "0" (U+0030)

ひらがな → カタカナ変換

export function toZenginKana(str: string): string {
    // ひらがな(U+3041-U+3096) → カタカナ(U+30A1-U+30F6)
    return str.replace(/[\u3041-\u3096]/g, (s) =>
        String.fromCharCode(s.charCodeAt(0) + 0x60)
    );
}

仕組み: Unicode上でひらがなとカタカナは 0x60 離れています。

"あ" (U+3042) + 0x60 = "ア" (U+30A2)
"か" (U+304B) + 0x60 = "カ" (U+30AB)

Zod スキーマでの活用

.transform() + .pipe() パターン

Zodの transform で前処理し、pipe でバリデーションに渡します:

import { z } from "zod";
import { toHalfWidth, toZenginKana } from "./normalize";

// 数字フィールド: 全角→半角変換してからバリデーション
const digitsOnly = (len: number, label: string) =>
    z.string()
        .transform(toHalfWidth)  // ★ 全角数字を半角に自動変換
        .pipe(
            z.string()
                .length(len, `${label}${len}桁です`)
                .regex(/^\d+$/, `${label}は半角数字のみです`)
        );

// カナフィールド: ひらがな→カタカナ変換してからバリデーション
const accountNameKanaSchema = z.string()
    .transform(toZenginKana)  // ★ ひらがなをカタカナに自動変換
    .pipe(
        z.string()
            .min(1, "口座名義(カナ)は必須です")
            .regex(
                /^[ァ-ヶー \s()().\-\/0-9A-Z]+$/,
                "口座名義はカナ・英数・記号のみです"
            )
    );

スキーマ全体

export const supplierCreateSchema = z.object({
    name: z.string().min(1, "仕入先名は必須です"),
    bankCode: digitsOnly(4, "金融機関コード"),       // "0005" → "0005" ✓
    branchCode: digitsOnly(3, "支店コード"),          // "012" → "012" ✓
    accountNumber: z.string()
        .transform(toHalfWidth)
        .pipe(
            z.string()
                .min(1, "口座番号は必須です")
                .max(7, "口座番号は最大7桁です")
                .regex(/^\d+$/, "口座番号は半角数字のみです")
        ),
    accountNameKana: accountNameKanaSchema,           // "かぶしきがいしゃ" → "カブシキガイシャ" ✓
    accountType: z.enum(["普通", "当座"]).default("普通"),
});

使用例

// API Route で使う
export async function POST(request: Request) {
    const body = await request.json();
    const result = supplierCreateSchema.safeParse(body);

    if (!result.success) {
        return NextResponse.json(
            { errors: result.error.flatten().fieldErrors },
            { status: 400 }
        );
    }

    // result.data は正規化済み
    // "0005" → "0005" に変換された状態
    const supplier = await prisma.supplier.create({
        data: result.data,
    });
}

Before / After

Before(transform なし)

ユーザー入力: "0005"
バリデーション: "半角数字のみです" ← エラー表示
ユーザー: 「え、半角ってなに...」(離脱)

After(transform あり)

ユーザー入力: "0005"
transform: "0005" に自動変換
バリデーション: ✓ 通過
ユーザー: (何も気づかない、ストレスゼロ)

フロントエンド側のバリデーション

サーバー側だけでなく、フロントエンドでもリアルタイムバリデーションを入れるとUXが向上します:

// React コンポーネント
const [bankCode, setBankCode] = useState("");
const [bankCodeError, setBankCodeError] = useState("");

const handleBankCodeChange = (value: string) => {
    setBankCode(value);
    const normalized = toHalfWidth(value);
    if (normalized && !/^\d{0,4}$/.test(normalized)) {
        setBankCodeError("金融機関コードは4桁の数字です");
    } else {
        setBankCodeError("");
    }
};

注意: フロントエンドのバリデーションはUX向上のためで、セキュリティはサーバー側のZodバリデーションで担保します。

テスト

正規化関数は入出力が明確なのでテストしやすい:

describe("toHalfWidth", () => {
    test("全角数字を半角に変換", () => {
        expect(toHalfWidth("0123")).toBe("0123");
    });
    test("全角英字を半角に変換", () => {
        expect(toHalfWidth("Abc")).toBe("Abc");
    });
    test("全角スペースを半角に変換", () => {
        expect(toHalfWidth("A B")).toBe("A B");
    });
    test("半角はそのまま", () => {
        expect(toHalfWidth("abc123")).toBe("abc123");
    });
    test("混在入力", () => {
        expect(toHalfWidth("1A2b3")).toBe("1A2b3");
    });
});

describe("toZenginKana", () => {
    test("ひらがなをカタカナに変換", () => {
        expect(toZenginKana("かぶしきがいしゃ")).toBe("カブシキガイシャ");
    });
    test("カタカナはそのまま", () => {
        expect(toZenginKana("カタカナ")).toBe("カタカナ");
    });
    test("混在入力", () => {
        expect(toZenginKana("かタかナ")).toBe("カタカナ");
    });
});

応用: 他のフィールドにも使える

フィールド 変換
電話番号 toHalfWidth + ハイフン除去
郵便番号 toHalfWidth + ハイフン除去
メールアドレス toHalfWidth + .toLowerCase()
住所のカナ toZenginKana
金額入力 toHalfWidth + カンマ除去

まとめ

  • 日本語フォームでは全角↔半角、ひらがな↔カタカナの表記揺れが頻発
  • Zod の transformpipe パターンで入力前に自動正規化
  • ユーザーにエラーを出さず、システム側で吸収するのがベストプラクティス
  • Unicode のコードポイント計算で変換関数は数行で書ける

この正規化パターンを使ったサービス: トルカ 振込アシスト — 請求書からAIで全銀フォーマットの振込データを自動生成。銀行コードや口座名義の入力も全角→半角を自動変換。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?