こんにちは、バックエンドエンジニアの日下です。
CSV から JSON へ変換するスクリプトを、TypeScript で実装する機会がありました。
今回は、CSV のデータのバリデーションに Zod を使った話をします。
スクリプトの目的
システム間のデータ連携が目的です。
連携元のシステムから CSV 出力されたデータを、連携先のシステムで利用する JSON へ変換します。
また、JSON への変換以外にも以下の要件があります。
- CSV のデータをバリデーションする
連携先のシステムで利用できるデータであることを保証するために、バリデーションを実行します。 - バリデーション失敗時に、日本語のエラーメッセージを表示する
スクリプトの実行は業務担当のエンドユーザーが行うため、日本語のエラーメッセージを表示します。
CSV の読み込み
元データとなる CSV の読み込みは、csv-parse を使用しました。
import fs from 'node:fs/promises'; import { parse } from 'csv-parse/sync'; // CSV ファイルの内容を InputData の配列に変換する const inputs: InputData[] = parse(await fs.readFile(csv), { // ヘッダー行を除いて読み込む from_line: 2, // 1行の各要素のプロパティ名を指定する columns: ['numbers', 'name'], });
CSV の各行の内容を、オブジェクトの配列に変換しています。
Zod とは
CSV から読み込んだオブジェクトのバリデーションには、Zod を使用しました。
Zod は、TypeScript ファーストのスキーマ宣言およびバリデーションライブリです。
なぜ Zod なのか?
宣言的な記述によりデータの情報を一覧できる
Zod ではオブジェクトのスキーマを宣言し、各プロパティごとのバリデーション・変換処理を記述できます。
これにより、オブジェクトに対して、以下の情報を一覧できます。
- どのようなプロパティが必要なのか
- 各プロパティは、どのような入力であるべきなのか
- 各プロパティが、どのように変換されるのか
import { z } from 'zod' // numbers, name プロパティを持つオブジェクト const InputDataSchema = z.object({ // 期待する入力データ例: "1,2,3" // バリデーション後に得られるデータ: [1, 2, 3] numbers: z .string() // 文字列であることを検証する .min(1) // 1文字以上であることを検証する .transform((value) => value.split(',')) // カンマ区切りの文字列を配列に変換する .pipe(z.array(z.coerce.number().positive())), // 配列の各要素を数値に変換し、正の数であることを検証する name: z.string().min(1), });
各プロパティに対するバリデーションが宣言的に書けるため、宣言したスキーマから必要な入力データが想像しやすいと考えています。
また、同時に変換処理も記述できるため、バリデーション同様にどのようなデータになるかもスキーマから把握しやすいです。
関連ツールが豊富である
Zod は関連ツールが豊富で、ドキュメントでも紹介されています。
今回の開発でも、以下2つのツールを利用しました。
- zod-validation-error
エラーメッセージのカスタマイズに使用しました。 - zod-i18n-map
エラーメッセージの日本語化に使用しました。
関連ツールを使用することで、要件にあったバリデーションを簡単に実現できるのも Zod の魅力です。
他ツールとの比較
Zod と同じくスキーマ宣言・バリデーションできるライブラリとして、Valibot があります。
今回は、以下 2 点の理由から Zod を採用しました。
- 使用事例や関連ツールが豊富であるから
Zod の方が先発であるため、使用事例や関連ツールを多く見つけられました。
同じバリデーションを実行する目的であれば、参考にできる事例・ツールを多く見つけられる方がスムーズに開発できると考えました。 - バンドルサイズを小さくする必要性が低かったから
Valibot の方がバンドルサイズは小さいことが謳われていますが、今回はあまり重要視しませんでした。
なぜなら、作成したスクリプトはサーバー上でビルド・実行するため、バンドルサイズを小さくする必要性が低かったからです。
CSV 用のエラーメッセージを組み立てる
CSV から読み取った内容はオブジェクトの配列になるため、バリデーションの実行で CSV 特有の事情を意識することはありませんでした。
しかし、CSV のバリデーションを行うために、エラーメッセージの組み立てで 2 点工夫したため紹介します。
どの行でエラーが発生したか把握できるようにする
なぜなら、CSV のどの行でエラーが発生したか把握できなければ、修正できないからです。
そのため、エラーメッセージに CSV の行番号を含めるようにしています。
今回、あらかじめ CSV から読み込んだオブジェクトに、行番号を記録するようにしました。
// Zod で定義したスキーマから型を抽出し、行番号のプロパティを追加する type InputData = z.input<typeof InputDataSchema> & { lineNo: number; }; const inputs: InputData[] = // CSV の読み込み処理 // 行番号を記録する for (const [index, input] of inputs.entries()) { // ヘッダー行をスキップした、2行目以降の行番号で記録する input.lineNo = index + 2; }
バリデーションエラー時は、記録していた行番号をエラーメッセージへ含めるようエラーを組み立てます。
エラーの組み立てには、zod-validation-error の fromZodError
を使用しました。
import { ZodError } from 'zod'; import { fromZodError } from 'zod-validation-error'; // CSV の各行の入力内容のバリデーション for (const input of inputs) { try { InputDataSchema.parse(input); } catch (err) { // エラーメッセージを組み立てる const validationError = fromZodError(err as ZodError, { // 1つ目のエラーメッセージの先頭に行番号を表示する prefix: `${input.lineNo}行目`, // 2つ目以降のエラーメッセージは改行し、1行目と同様に行番号を表示する issueSeparator: `\n${input.lineNo}行目: `, // エラーが発生したプロパティ名をエラーメッセージのサフィックスに含めない // なぜなら、日本語でエラーを表示するため、英語のプロパティ名は表示しないから includePath: false, }); // エラーを配列の validationErrors に追加する validationErrors.push(validationError); } }
仮に CSV の 3 行目に 2 つのエラーが存在した場合、以下のようなエラーメッセージを得られます。
3行目: <1つ目のエラー内容> 3行目: <2つ目のエラー内容>
行中のどの要素でエラーが発生したか把握できるようにする
なぜなら、CSV の 1 行には複数の要素が含まれているからです。
どの要素でエラーが発生したか把握できなければ、修正できません。
今回は、日本語訳のために使用した zod-i18n-map の handlePath
の機能を使い、エラーメッセージに要素名を含めました。
import { init } from 'i18next'; import { makeZodI18nMap } from 'zod-i18n-map'; // 日本語訳を設定する await init({ lng: 'ja', resources: { ja: { zod: { errors: { too_small: { string: { inclusive_with_path: '{{path}}は{{minimum}}文字以上の文字列である必要があります。', }, }, }, }, inputData: { numbers: '数値', name: '名前', }, }, }, }); // CSV の各行の入力内容のバリデーション for (const input of inputs) { try { InputDataSchema.parse(input, { // zod-i18n-map を設定する errorMap: makeZodI18nMap({ // 使用する日本語訳のネームスペースを指定する ns: ['zod', 'inputData'], }), }); } catch (err) { // エラーメッセージの組み立て } }
name
に 1 文字以上の文字列であるバリデーションを設定している場合、name
が 0 文字の時は以下のエラーメッセージが得られます。
名前は1文字以上の文字列である必要があります。
まとめ
Zod を使って CSV に対するバリデーションを実施し、システム間のデータの連携先へ不備のない JSON を提供できました。
また、CSV 用のエラーメッセージの組み立ても Zod の関連ツールを使用することで、簡易に実現できました。
We are hiring
株式会社ドワンゴの教育事業では、一緒に未来の当たり前の教育をつくるメンバーを募集しています。
カジュアル面談も行っています。
お気軽にご連絡ください!
筆者は普段 Ruby を使って、業務では初めて TypeScript を使いました。
このように、普段使っていない言語へ挑戦する機会もあります!
開発チームの取り組み、教育事業の今後については、他の記事や採用資料をご覧ください。