Zod で配列・オブジェクトを安全に扱うバリデーションテクニック

Web 開発において、JavaScript や TypeScript で配列やオブジェクトを扱う際、実行時エラーに悩まされた経験はありませんか? TypeScript の型チェックだけでは防げないランタイムエラーを、Zod を活用することで確実に防ぐことができます。
今回は、実際の開発現場でよく遭遇する配列・オブジェクト操作の問題を解決する Zod のバリデーションテクニックをご紹介します。 実践的なコード例とともに、安全で堅牢なアプリケーションを構築する方法を学んでいきましょう。
背景
TypeScript 型安全性の限界
TypeScript は静的型チェックにより開発時の型安全性を提供してくれますが、実行時のデータに対しては無力です。 特に外部 API からのレスポンスやユーザー入力データなど、実行時に動的に取得されるデータは型情報が保証されません。
以下のような問題が発生することがあります。
typescript// TypeScriptでは問題なくコンパイルされるが...
interface User {
id: number;
name: string;
email: string;
}
// APIから取得したデータが期待通りの型であるとは限らない
const userData: User = await fetchUserData();
console.log(userData.name.toUpperCase()); // 実行時エラーの可能性
実行時バリデーションの必要性
現代の Web アプリケーションでは、様々なデータソースから情報を取得します。 これらのデータが常に期待通りの形式で提供される保証はありません。
typescript// 外部APIからの不安定なデータ例
const apiResponse = {
users: null, // 配列のはずがnull
metadata: {
// countプロパティが欠如
},
};
実行時バリデーションを実装することで、このような予期しないデータ構造によるエラーを防ぐことができるのです。
配列・オブジェクトで発生しがちな問題
実際の開発現場では以下のような問題に直面することが多いでしょう。
typescript// よくある問題例
const processUserList = (users: any[]) => {
// 配列が空の場合やnullの場合の考慮不足
return users.map((user) => user.name.toUpperCase()); // TypeError: Cannot read property 'name' of null
};
// オブジェクトのプロパティ存在チェック不足
const getUserEmail = (user: any) => {
return user.contact.email; // TypeError: Cannot read property 'email' of undefined
};
これらの問題を図で整理すると以下のようになります。
mermaidflowchart TD
data[外部データ] --> check{型チェック}
check -->|TypeScriptのみ| compile[コンパイル時チェック]
check -->|Zod使用| runtime[実行時チェック]
compile --> risk[実行時エラーリスク]
runtime --> safe[型安全な処理]
risk --> error1[undefined プロパティアクセス]
risk --> error2[null参照エラー]
risk --> error3[型不一致による処理エラー]
safe --> validate[データ検証]
safe --> transform[型変換・正規化]
safe --> process[安全な処理実行]
課題
未定義プロパティへのアクセス
オブジェクトのプロパティにアクセスする際、そのプロパティが存在しない場合に発生するエラーは非常に一般的です。
typescript// 問題のあるコード例
interface UserProfile {
personalInfo: {
firstName: string;
lastName: string;
address?: {
street: string;
city: string;
};
};
}
const displayUserAddress = (profile: UserProfile) => {
// addressがundefinedの場合にエラーが発生
return `${profile.personalInfo.address.street}, ${profile.personalInfo.address.city}`;
};
この問題は、オプショナルプロパティや深くネストしたオブジェクト構造で特に顕著に現れます。
配列の要素型不一致
配列を扱う際、要素の型が期待通りでない場合に発生する問題も深刻です。
typescript// 配列要素の型が統一されていない場合
const processNumbers = (numbers: number[]) => {
return numbers.map((num) => num * 2); // 文字列が混じっているとNaNが発生
};
// 実際のデータ
const mixedArray = [1, '2', 3, null, 4]; // 型が混在
processNumbers(mixedArray as number[]); // 危険なキャスト
ネストしたオブジェクトの検証漏れ
複雑な構造を持つオブジェクトでは、深い階層での検証が漏れがちです。
typescript// 複雑なデータ構造の例
interface ApiResponse {
status: string;
data: {
users: Array<{
id: number;
profile: {
personal: {
name: string;
contacts: {
emails: string[];
phones: string[];
};
};
preferences: {
notifications: boolean;
privacy: {
shareData: boolean;
};
};
};
}>;
};
}
このような構造では、各階層での型検証を適切に行うことが困難になります。
以下の図は、これらの課題の関連性を示しています。
mermaidflowchart LR
subgraph problems[よくある問題]
prop[未定義プロパティ]
array[配列要素型不一致]
nest[ネスト構造検証漏れ]
end
subgraph causes[原因]
runtime[実行時データの不確実性]
complex[複雑なデータ構造]
validation[検証処理の不備]
end
subgraph results[結果]
crash[アプリケーションクラッシュ]
data_corruption[データ破損]
user_error[ユーザーエクスペリエンス低下]
end
causes --> problems
problems --> results
解決策
Zod による基本的な配列検証
Zod を使用することで、配列の要素型を厳密にチェックし、安全な操作を保証できます。
typescriptimport { z } from 'zod';
// 基本的な配列スキーマの定義
const NumberArraySchema = z.array(z.number());
const StringArraySchema = z.array(z.string());
配列要素数の制限も簡単に設定できます。
typescript// 配列要素数の制限
const LimitedArraySchema = z
.array(z.string())
.min(1, '配列は少なくとも1つの要素が必要です')
.max(10, '配列の要素数は10個までです');
実際の検証処理では以下のようになります。
typescriptconst validateNumberArray = (data: unknown) => {
try {
const validatedArray = NumberArraySchema.parse(data);
return validatedArray; // 型安全な number[] が返される
} catch (error) {
if (error instanceof z.ZodError) {
console.error('バリデーションエラー:', error.errors);
}
throw error;
}
};
// 使用例
const processNumbers = (input: unknown) => {
const numbers = validateNumberArray(input);
// この時点で numbers は確実に number[] 型
return numbers.map((num) => num * 2);
};
オブジェクトスキーマ定義
オブジェクトの構造を明確に定義し、プロパティの存在と型を検証します。
typescript// ユーザーオブジェクトのスキーマ定義
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z
.string()
.email('有効なメールアドレスを入力してください'),
age: z.number().min(0).max(120),
isActive: z.boolean().default(true),
});
オプショナルプロパティの定義も簡潔です。
typescript// オプショナルプロパティを含むスキーマ
const UserProfileSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
phone: z.string().optional(), // オプショナル
address: z
.object({
street: z.string(),
city: z.string(),
postalCode: z
.string()
.regex(
/^\d{3}-\d{4}$/,
'郵便番号は###-####の形式で入力してください'
),
})
.optional(),
});
type UserProfile = z.infer<typeof UserProfileSchema>;
ネストした構造の安全な検証
複雑なネスト構造も段階的にスキーマを定義することで安全に処理できます。
typescript// 段階的なスキーマ定義
const ContactSchema = z.object({
emails: z.array(z.string().email()),
phones: z.array(z.string()),
});
const PersonalInfoSchema = z.object({
name: z.string(),
contacts: ContactSchema,
});
const PreferencesSchema = z.object({
notifications: z.boolean(),
privacy: z.object({
shareData: z.boolean(),
}),
});
これらを組み合わせて最終的な構造を定義します。
typescriptconst UserCompleteSchema = z.object({
id: z.number(),
profile: z.object({
personal: PersonalInfoSchema,
preferences: PreferencesSchema,
}),
});
const ApiResponseSchema = z.object({
status: z.string(),
data: z.object({
users: z.array(UserCompleteSchema),
}),
});
以下の図は、Zod による解決策のアプローチを示しています。
mermaidflowchart TD
input[入力データ] --> zodSchema[Zodスキーマ]
zodSchema --> validation{バリデーション}
validation -->|成功| validated[検証済みデータ]
validation -->|失敗| error[エラーハンドリング]
validated --> process[安全な処理]
error --> retry[リトライ処理]
error --> fallback[フォールバック処理]
error --> log[エラーログ]
subgraph zodFeatures[Zodの機能]
transform[データ変換]
typeInfer[型推論]
errorMsg[詳細エラーメッセージ]
end
zodSchema --> zodFeatures
具体例
配列要素の型チェック実装
API から取得したユーザーリストを安全に処理する実装例をご紹介します。
typescriptimport { z } from 'zod';
// ユーザーデータのスキーマ定義
const UserSchema = z.object({
id: z.number(),
name: z.string().min(1, '名前は必須です'),
email: z
.string()
.email('有効なメールアドレスを入力してください'),
role: z.enum(['admin', 'user', 'guest']),
});
const UsersArraySchema = z.array(UserSchema);
type User = z.infer<typeof UserSchema>;
実際のバリデーション処理では、エラーハンドリングを含めた実装を行います。
typescriptconst processUserList = async (
apiData: unknown
): Promise<User[]> => {
try {
// Zodによるバリデーション
const validatedUsers = UsersArraySchema.parse(apiData);
console.log(
`${validatedUsers.length}人のユーザーを処理します`
);
return validatedUsers;
} catch (error) {
if (error instanceof z.ZodError) {
// 詳細なエラー情報を表示
error.errors.forEach((err, index) => {
console.error(`エラー ${index + 1}:`, {
path: err.path.join('.'),
message: err.message,
received: err.received,
});
});
}
throw new Error('ユーザーデータの形式が不正です');
}
};
使用例とエラーケースの処理方法も見てみましょう。
typescript// API呼び出しとバリデーション
const fetchAndProcessUsers = async () => {
try {
const response = await fetch('/api/users');
const rawData = await response.json();
const users = await processUserList(rawData);
// この時点で users は型安全な User[] として使用可能
const adminUsers = users.filter(
(user) => user.role === 'admin'
);
const userEmails = users.map((user) => user.email);
return { users, adminUsers, userEmails };
} catch (error) {
console.error('ユーザー処理でエラーが発生:', error);
// フォールバック処理
return { users: [], adminUsers: [], userEmails: [] };
}
};
オブジェクトのプロパティ検証
フォームデータの検証を例に、オブジェクトの安全な処理方法を説明します。
typescript// フォームデータのスキーマ定義
const ContactFormSchema = z.object({
name: z
.string()
.min(1, 'お名前は必須です')
.max(50, 'お名前は50文字以内で入力してください'),
email: z
.string()
.email('有効なメールアドレスを入力してください'),
phone: z
.string()
.regex(
/^0\d{1,4}-\d{1,4}-\d{4}$/,
'電話番号の形式が正しくありません'
)
.optional(),
message: z
.string()
.min(10, 'メッセージは10文字以上で入力してください')
.max(
1000,
'メッセージは1000文字以内で入力してください'
),
agreeToTerms: z.boolean().refine((val) => val === true, {
message: '利用規約への同意が必要です',
}),
});
type ContactForm = z.infer<typeof ContactFormSchema>;
バリデーション関数の実装では、エラーメッセージを分かりやすく整形します。
typescriptconst validateContactForm = (formData: unknown) => {
const result = ContactFormSchema.safeParse(formData);
if (!result.success) {
// エラーを項目別に整理
const fieldErrors: Record<string, string> = {};
result.error.errors.forEach((error) => {
const fieldName = error.path[0] as string;
if (!fieldErrors[fieldName]) {
fieldErrors[fieldName] = error.message;
}
});
return {
success: false,
errors: fieldErrors,
data: null,
};
}
return {
success: true,
errors: {},
data: result.data,
};
};
実際のフォーム処理では以下のように使用します。
typescriptconst handleFormSubmission = async (
rawFormData: FormData
) => {
// FormDataをオブジェクトに変換
const formObject = Object.fromEntries(
rawFormData.entries()
);
// チェックボックスのブール値変換
formObject.agreeToTerms =
formObject.agreeToTerms === 'on';
const validationResult = validateContactForm(formObject);
if (!validationResult.success) {
// バリデーションエラーの場合
return {
status: 'error',
message: 'フォームに入力エラーがあります',
fieldErrors: validationResult.errors,
};
}
// 検証済みデータを使用
const contactData = validationResult.data;
try {
// データベースへの保存処理
await saveContactData(contactData);
return {
status: 'success',
message: 'お問い合わせを受付ました',
};
} catch (error) {
return {
status: 'error',
message: 'サーバーエラーが発生しました',
};
}
};
複雑なデータ構造の検証
EC サイトの注文データのような複雑な構造を扱う例を見てみましょう。
typescript// 商品アイテムのスキーマ
const OrderItemSchema = z.object({
productId: z.number(),
productName: z.string(),
quantity: z
.number()
.min(1, '数量は1以上である必要があります'),
unitPrice: z.number().min(0),
totalPrice: z.number().min(0),
});
// 配送先情報のスキーマ
const ShippingAddressSchema = z.object({
name: z.string().min(1),
postalCode: z.string().regex(/^\d{3}-\d{4}$/),
prefecture: z.string(),
city: z.string(),
streetAddress: z.string(),
building: z.string().optional(),
});
これらを組み合わせて注文全体のスキーマを定義します。
typescript// 注文データ全体のスキーマ
const OrderSchema = z
.object({
orderId: z
.string()
.uuid('注文IDの形式が正しくありません'),
customerId: z.number(),
orderDate: z
.string()
.datetime('日時の形式が正しくありません'),
items: z
.array(OrderItemSchema)
.min(1, '注文には少なくとも1つの商品が必要です'),
shippingAddress: ShippingAddressSchema,
paymentMethod: z.enum([
'credit_card',
'bank_transfer',
'cash_on_delivery',
]),
totalAmount: z.number().min(0),
status: z.enum([
'pending',
'confirmed',
'shipped',
'delivered',
'cancelled',
]),
})
.refine(
(data) => {
// 合計金額の整合性チェック
const calculatedTotal = data.items.reduce(
(sum, item) => sum + item.totalPrice,
0
);
return (
Math.abs(calculatedTotal - data.totalAmount) < 0.01
);
},
{
message: '合計金額が商品価格の合計と一致しません',
path: ['totalAmount'],
}
);
type Order = z.infer<typeof OrderSchema>;
エラーハンドリングパターン
実用的なエラーハンドリングのパターンをいくつか紹介します。
typescript// パターン1: 段階的バリデーション
const validateOrderWithFallback = (data: unknown) => {
// まず基本的な構造をチェック
const basicStructure = z.object({
orderId: z.string(),
customerId: z.number(),
items: z.array(z.unknown()),
});
try {
basicStructure.parse(data);
} catch {
return {
success: false,
error: '基本的なデータ構造が不正です',
};
}
// 詳細なバリデーションを実行
const result = OrderSchema.safeParse(data);
if (!result.success) {
return {
success: false,
error: '詳細バリデーションに失敗',
details: result.error.errors,
};
}
return { success: true, data: result.data };
};
typescript// パターン2: 部分的な修復処理
const validateAndRepairOrder = (data: unknown) => {
const result = OrderSchema.safeParse(data);
if (result.success) {
return {
success: true,
data: result.data,
repaired: false,
};
}
// 修復可能なエラーをチェック
let repairedData = { ...(data as any) };
let wasRepaired = false;
result.error.errors.forEach((error) => {
if (
error.path.includes('orderDate') &&
error.code === 'invalid_string'
) {
// 日付形式の修復
repairedData.orderDate = new Date().toISOString();
wasRepaired = true;
}
if (
error.path.includes('status') &&
error.code === 'invalid_enum_value'
) {
// ステータスのデフォルト設定
repairedData.status = 'pending';
wasRepaired = true;
}
});
if (wasRepaired) {
const repairResult =
OrderSchema.safeParse(repairedData);
if (repairResult.success) {
return {
success: true,
data: repairResult.data,
repaired: true,
};
}
}
return { success: false, error: result.error };
};
以下の図は、複雑なデータ構造の検証フローを示しています。
mermaidsequenceDiagram
participant Client as クライアント
participant Validator as バリデーター
participant Schema as Zodスキーマ
participant DB as データベース
Client ->> Validator: 注文データ送信
Validator ->> Schema: 基本構造チェック
Schema -->> Validator: 構造OK/NG
alt 基本構造OK
Validator ->> Schema: 詳細バリデーション
Schema -->> Validator: 詳細チェック結果
alt 詳細バリデーションOK
Validator ->> DB: データ保存
DB -->> Validator: 保存完了
Validator -->> Client: 成功レスポンス
else 詳細バリデーションNG
Validator ->> Validator: 修復処理試行
alt 修復成功
Validator ->> DB: 修復データ保存
DB -->> Validator: 保存完了
Validator -->> Client: 修復完了レスポンス
else 修復失敗
Validator -->> Client: エラーレスポンス
end
end
else 基本構造NG
Validator -->> Client: 構造エラーレスポンス
end
まとめ
Zod による安全な配列・オブジェクト操作のポイント
今回ご紹介した Zod を活用したバリデーションテクニックにより、以下のメリットを実現できます。
型安全性の向上
Zod のスキーマ定義により、TypeScript の静的型チェックと実行時バリデーションの両方を活用できます。
z.infer
を使用することで、スキーマから自動的に型を生成し、コードの保守性も向上します。
エラーの早期発見 データの不整合や型不一致を実行時に早期発見することで、アプリケーションのクラッシュを防げます。 詳細なエラーメッセージにより、問題の特定と修正も効率的に行えるでしょう。
保守性の向上 スキーマを中心とした設計により、データ構造の変更時の影響範囲が明確になります。 段階的なスキーマ定義により、複雑な構造も理解しやすく保てるのです。
実践で活用する際の重要なポイント
段階的な導入 既存プロジェクトに Zod を導入する際は、重要度の高い API エンドポイントやフォーム処理から段階的に適用することをお勧めします。
エラーハンドリングの充実
バリデーションエラーが発生した場合の適切な処理フローを設計し、ユーザーエクスペリエンスを損なわないよう配慮することが大切です。
パフォーマンスへの配慮 大量のデータを処理する場合は、バリデーション処理のパフォーマンスに注意を払い、必要に応じて部分的バリデーションや非同期処理を検討しましょう。
Zod を活用することで、より安全で信頼性の高い Web アプリケーションを構築できます。 今回ご紹介したテクニックを参考に、ぜひ実際のプロジェクトで Zod を活用してみてください。
関連リンク
- article
Zod で配列・オブジェクトを安全に扱うバリデーションテクニック
- article
【実践】Zod の union・discriminatedUnion を使った柔軟な型定義
- article
Zod で条件付きバリデーションを実装する方法(if/then/else パターン)
- article
【徹底解説】Zod の refine と superRefine の違いと実践活用シーン
- article
Zod と React Hook Form を組み合わせて使う方法と実装例
- article
【入門】Zod で型安全なフォームバリデーションを実現する基本ステップ
- article
Astro と Tailwind CSS で美しいデザインを最速実現
- article
shadcn/ui のコンポーネント一覧と使い方まとめ
- article
Apollo Client の Reactive Variables - GraphQL でグローバル状態管理
- article
Remix でデータフェッチ最適化:Loader のベストプラクティス
- article
ゼロから始める Preact 開発 - セットアップから初回デプロイまで
- article
Zod で配列・オブジェクトを安全に扱うバリデーションテクニック
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- blog
失敗を称賛する文化はどう作る?アジャイルな組織へ生まれ変わるための第一歩
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来