TypeScriptのCDKスタックをZodでバリデーションする

概要

TypeScriptで書かれたCDKスタックのバリデーションを、Zodというバリデーションツールでバリデートするようにしてみました。


目次

目次


前提

TypeScriptでCDKを使用しています。

また、CDKはv2を使用しております。

❯ cdk --version
2.31.0 (build b67950d)


Zodとは

ざっくり言うと、TypeScriptのバリデーションツールです。

github.com


リクエストなどのオブジェクト情報を、定義したtypeのオブジェクトにパースするタイミングで、定義したルールに基づいてバリデーションを行えるツールです。

type定義と同じファイルでバリデーションルールが書けるので、非常に楽にバリデーションを行うことができます。

import { z } from "zod";

export const RequestSchema = z.object({
  title: z.number(),
  message: z.string().min(1),
  mailAddress: z.string().email().min(1),
  tags: z.array(z.string().min(1)).optional(),
});

export type Request = z.infer<typeof RequestSchema>;
// ここでバリデーションも同時に行われる
const request: Request = RequestSchema.parse(body);


上記のように、型・オプショナル(.optional())・空文字チェック(.min(1))だけでなく、email形式のハンドリング(.email())なども行うことができます。


バリデーションエラー時は、以下のようなエラーが出力されます。

{
  "issues": [
    {
      "validation": "email",
      "code": "invalid_string",
      "message": "Invalid email",
      "path": [
        "mailAddress"
      ]
    }
  ],
  "name": "ZodError"
}


コード例

さっそくコード例です。

メインとなる3ファイルを例として取り上げます。

  • スタッククラス
  • バリデータークラス
  • Zodスキーマ


下記のコードでバリデーションを実装すると、定義したバリデーションがcdk synth時に行われます。


スタッククラス

テキトーな例ですが、init()でバリデート、create()でリソースの作成を行なっています。

export class SampleStack extends Stack {
  private topicName: string;
  private mailAddress: string;

  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    this.init();
    this.create();
  }

  private init(): void {
    this.topicName = process.env.topicName;
    this.mailAddress = process.env.mailAddress;

    const stackValidator = new StackValidator(
      this.topicName,
      this.mailAddress,
    );
    this.node.addValidation(stackValidator);
  }

  private create() {
  // ...(省略)


init()内でバリデータークラスのインスタンスを生成バリデート対象の情報をコンストラクタで渡しthis.node.addValidationというメソッドにインスタンスを渡しています。

これはCDKで提供されているバリデートメソッドで、複数のバリデーションが登録可能で、cdk synth時にバリデーションが走ってくれるイメージです。

詳細は以下の記事で説明しています。

go-to-k.hatenablog.com


バリデータークラス

こちらは具体的なバリデート処理を書くクラスで、validate()内でZodによるバリデーション処理を行なっています。

このようにバリデータークラスとしてスタッククラスから切り出すことによって、もしZodを使うのをやめてバリデート方法を変える場合もスタッククラス自体には手を加えなくて良いため、保守性が高くなるメリットがあります。

import { IValidation } from "constructs";
import { StackInput, StackInputSchema } from "../types/stack-input";

export class StackValidator implements IValidation {
  private topicName: string;
  private mailAddress: string;

  constructor(topicName: string, mailAddress: string) {
    this.topicName = topicName;
    this.mailAddress = mailAddress;
  }

  public validate(): string[] {
    const errors: string[] = [];

    try {
      const stackInput: StackInput = StackInputSchema.parse({
        topicName: this.topicName,
        mailAddress: this.mailAddress,
      });
    } catch (e) {
      errors.push(JSON.stringify(e));
    }

    return errors;
  }
}


後述するZodのスキーマファイルで定義したスキーマのparseメソッドにバリデーション対象のオブジェクトを渡すことで、パースとバリデーションを一緒に行ってくれます。

成功すれば、同じく定義するtypeの変数にオブジェクトが格納され、失敗すればZodのバリデーションエラーが吐かれます。

      const stackInput: StackInput = StackInputSchema.parse({
        topicName: this.topicName,
        mailAddress: this.mailAddress,
      });


Zodスキーマ

こちらでZodスキーマ・typeを定義し、Zodによるバリデーションルールスキーマのプロパティに対して定義します。

import { z } from "zod";

export const StackInputSchema = z.object({
  topicName: z.string().min(1),
  mailAddress: z.string().email().min(1),
});

export type StackInput = z.infer<typeof StackInputSchema>;


コンパイル失敗!

Zodを使ってCDKのバリデーションを行うことに関しては以上なのですが、実際に動かしてみるとコンパイルに失敗してしまいました。


エラーメッセージ

✦ ❯ cdk synth
⨯ Unable to compile TypeScript:
lib/types/stack-input.ts:3:33 - error TS2589: Type instantiation is excessively deep and possibly infinite.

  3 export const StackInputSchema = z.object({
                                    ~~~~~~~~~~
  4   topicName: z.string().min(1),
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  5   mailAddress: z.string().email().min(1),
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  6 });
    ~~


Subprocess exited with error 1


バージョン

突然なのですが、ここでバージョンを確認してみましょう。

  • Zod : 3.17.3
  • TypeScript : 3.9.10


解決

説明をすごく端折りますが、TypeScriptのバージョンをあげたら解決しました(Zodはそのまま)。

  • Zod : 3.17.3
  • TypeScript : 3.9.10 -> 4.7.4


※上記エラーでググったらissueなどが出たのですが、ここでは触れません。


バリデーション実行

それでは、早速わざとメールアドレスの形式を不適切(abcde.fgh)にして、cdk synthを実行してみました。

/Users/goto/github/mail-queues/node_modules/aws-cdk-lib/core/lib/private/synthesis.js:2
  `);throw new Error(`Validation failed with the following errors:
           ^
Error: Validation failed with the following errors:
  [SampleStack] {"issues":[{"validation":"email","code":"invalid_string","message":"Invalid email","path":["senderAddress"]}],"name":"ZodError"}
    at validateTree (/Users/goto/github/mail-queues/node_modules/aws-cdk-lib/core/lib/private/synthesis.js:2:12)
    at Object.synthesize (/Users/goto/github/mail-queues/node_modules/aws-cdk-lib/core/lib/private/synthesis.js:1:598)
    at App.synth (/Users/goto/github/mail-queues/node_modules/aws-cdk-lib/core/lib/stage.js:1:1866)
    at process.<anonymous> (/Users/goto/github/mail-queues/node_modules/aws-cdk-lib/core/lib/app.js:1:1164)
    at Object.onceWrapper (events.js:520:26)
    at process.emit (events.js:400:28)
    at process.emit (domain.js:475:12)
    at process.emit.sharedData.processEmitHook.installedValue [as emit] (/Users/goto/github/mail-queues/node_modules/@cspotcode/source-map-support/source-map-support.js:745:40)

Subprocess exited with error 1


Zodのスキーマで定義した通り、emailバリデーションに引っかかってくれて、エラーが出力されました。

めでたしめでたし。


最後に

前回CDKでバリデーションを行う記事を書いたのですが、今回はさらにTypeScriptにフォーカスを置いて、便利なZodを入れてみました。

やっぱりバリデーションをフレームワークに任せるのは楽ですね。


Twitter始めました!

良かったらぜひ。お知り合い増やせたら嬉しいです。

Twitter ID → @365_step_tech