SvelteKitのフォームバリデーションにZodを導入した話

フォームのバリデーションをif文で手書きすると項目が増えるたびにコードが複雑になります。Zodを使うとスキーマで宣言的に書けます。


インストール

npm install zod

Zodなしのバリデーション(if文が増えていく)

const title = data.get('title') as string;
const content = data.get('content') as string;

if (!title) return fail(400, { error: 'タイトルは必須です' });
if (title.length > 100) return fail(400, { error: 'タイトルは100文字以内' });
if (!content) return fail(400, { error: '本文は必須です' });
if (content.length < 10) return fail(400, { error: '本文は10文字以上' });
// 項目が増えるたびにif文が増える...

Zodを使うとスキーマで宣言的に書ける

// src/routes/learn/+page.server.ts
import { fail } from '@sveltejs/kit';
import { z } from 'zod';
import type { Actions } from './$types';

const TextSchema = z.object({
  text: z.string()
    .min(1, 'テキストを入力してください')
    .max(50, '50文字以内で入力してください'),
});

export const actions: Actions = {
  default: async ({ request }) => {
    const data = await request.formData();

    const result = TextSchema.safeParse({
      text: data.get('text'),
    });

    if (!result.success) {
      const errors = z.flattenError(result.error).fieldErrors;
      return fail(400, {
        errors,
        text: data.get('text') as string,
      });
    }

    // result.data は型が保証されている
    const { text } = result.data;
    // DB に保存する処理...

    return { success: true };
  },
};

ポイント:z.flattenError()

Zod v4 から error.flatten() が非推奨になり、z.flattenError(error) に変わりました。返ってくる fieldErrors の構造は同じです。

// ❌ Zod v4 では非推奨
const errors = result.error.flatten().fieldErrors;

// ✅ Zod v4 以降はこちら
const errors = z.flattenError(result.error).fieldErrors;

エラーを画面に表示する

<!-- src/routes/learn/+page.svelte -->
<script lang="ts">
  import { enhance } from "$app/forms";
  import type { ActionData } from "./$types";

  type Props = { form: ActionData };
  let { form }: Props = $props();
  let submitting = $state(false);
</script>

<form
  method="POST"
  use:enhance={() => {
    submitting = true;
    return async ({ update }) => {
      await update();
      submitting = false;
    };
  }}
>
  <input name="text" value={form?.text ?? ""} placeholder="タスクを入力(50文字以内)" />
  {#if form?.errors?.text}
    <p style="color: red">{form.errors.text[0]}</p>
  {/if}
  <button disabled={submitting}>
    {submitting ? "送信中…" : "追加"}
  </button>
</form>

fieldErrors はフィールドごとにエラーメッセージの配列を返します。[0] で最初のエラーを表示します。


よく使うZodのメソッド

// 文字列
z.string().min(1)           // 最小文字数
z.string().max(100)         // 最大文字数
z.string().email()          // メール形式
z.string().url()            // URL形式
z.string().regex(/^\d+$/)   // 正規表現

// 数値
z.number().min(0)           // 最小値
z.number().max(100)         // 最大値
z.number().int()            // 整数のみ

// その他
z.boolean()
z.enum(['draft', 'published'])  // 特定の値のみ
z.optional(z.string())          // 省略可能

まとめ

if文での手書き Zod
バリデーションルールの場所 actionsの中に散在 スキーマに集約
TypeScriptの型 手動でキャスト result.data に自動で付く
エラーの管理 自前で構造を作る fieldErrors で自動分類
項目が増えたとき if文が増える スキーマにフィールドを追加するだけ
← トップページに戻る