フォーム入力情報からZodを利用してDTO作成しへ変換処理を実施するやり方を紹介

Zodはバリデーションと型定義だけでなく、入力データを任意の出力形式に変換する機能(DTO)も備えています。
これにより、フォームのUI層では扱いやすいデータ型で管理し、バックエンドに送信するDTO(Data Transfer Object)をシームレスに橋渡しすることができます。
このドキュメントでは、フォーム入力 → DTOへの変換パターンを豊富な例とともに解説します。
基本:.transform()
を使った変換
z.string().transform(fn)
などの記法で、値を別の形式に変換できます。
例:文字列の数値入力 → numberへ変換
tsconst formSchema = z.object({
price: z.string().transform((val) => parseFloat(val)),
});
type Dto = z.infer<typeof formSchema>;
const result = formSchema.parse({ price: "123.45" });
// result.price は number型(123.45)
複数フィールド → 単一の構造に変換(例:名前統合)
tsconst formSchema = z
.object({
firstName: z.string(),
lastName: z.string(),
})
.transform((val) => ({
fullName: `${val.lastName} ${val.firstName}`,
}));
type NameDto = z.infer<typeof formSchema>;
// NameDto = { fullName: string }
入力補助形式 → サーバー形式(例:日付)
tsconst formSchema = z.object({
birthday: z
.string()
.refine((val) => /^\d{4}-\d{2}-\d{2}$/.test(val), {
message: "日付はYYYY-MM-DD形式で入力してください",
})
.transform((val) => new Date(val)),
});
type BirthdayDto = z.infer<typeof formSchema>;
// BirthdayDto = { birthday: Date }
nested .transform()
と .refine()
の組み合わせ
tsconst formSchema = z
.object({
height: z.string().transform((val) => parseInt(val, 10)),
weight: z.string().transform((val) => parseInt(val, 10)),
})
.refine(({ height, weight }) => height > 0 && weight > 0, {
message: "身長と体重は正の数である必要があります",
});
type BodyDto = z.infer<typeof formSchema>;
// BodyDto = { height: number; weight: number }
状態(チェックボックスやセレクト)からDTO形式へ変換
例:チェックボックス → フラグ配列へ変換
tsconst formSchema = z
.object({
apple: z.boolean(),
banana: z.boolean(),
orange: z.boolean(),
})
.transform((val) => {
const selected: string[] = [];
if (val.apple) selected.push("apple");
if (val.banana) selected.push("banana");
if (val.orange) selected.push("orange");
return { selectedFruits: selected };
});
type FruitDto = z.infer<typeof formSchema>;
// FruitDto = { selectedFruits: string[] }
日付・時間を分割入力 → 1つのDate型へ変換
tsconst formSchema = z
.object({
year: z.string(),
month: z.string(),
day: z.string(),
})
.transform((val) => {
const { year, month, day } = val;
return {
birthdate: new Date(`${year}-${month}-${day}`),
};
});
type DateDto = z.infer<typeof formSchema>;
// DateDto = { birthdate: Date }
入力型(フォーム用)とDTO型を明確に分けて使うパターン
Zodでは、入力型と出力型(変換後のDTO)を明示的に分けることも可能です。
入力スキーマ(string型)
tsconst rawFormSchema = z.object({
age: z.string(),
});
DTOスキーマ(number型に変換)
tsconst dtoSchema = rawFormSchema.transform((val) => ({
age: parseInt(val.age, 10),
}));
type FormInput = z.input<typeof dtoSchema>; // { age: string }
type FormDto = z.output<typeof dtoSchema>; // { age: number }
この方法により、UIと送信データの責任を分離しながら、型整合性を維持できます。
.transform()
をネストオブジェクトで使う例
tsconst formSchema = z
.object({
profile: z.object({
age: z.string(),
nickname: z.string(),
}),
})
.transform((val) => ({
profile: {
age: Number(val.profile.age),
nickname: val.profile.nickname,
},
}));
type ProfileDto = z.infer<typeof formSchema>;
// ProfileDto = { profile: { age: number; nickname: string } }
.pipe()
を使って変換とバリデーションを分離
pipe()
は.transform()
と違い、変換後の型に対してさらにバリデーションを適用できます。
tsconst schema = z
.string()
.transform((val) => parseFloat(val))
.pipe(z.number().positive());
schema.parse("100"); // OK → 100
schema.parse("-5"); // ❌ → バリデーションエラー
まとめ
Zodを使えば、フォーム入力データをDTO形式に変換しながら、同時に型安全性・妥当性も確保できます。
技法 | 目的 |
---|---|
.transform() | 値の直接変換(文字列→数値など) |
.refine() / .superRefine() | バリデーションロジックの追加 |
.pipe() | 変換後にさらに制約を加えたいとき |
z.input<> / z.output<> | 入力型と出力型を明確に分離 |
こうしたパターンを活用することで、UIは柔軟に・サーバーには堅牢にという理想的なデータ設計が実現できます。