Validate TypeScript CDK stack with Zod

k_goto

k.goto

Posted on June 18, 2023

Validate TypeScript CDK stack with Zod

I have tried to validate CDK stacks written in TypeScript with a validation tool called Zod.


Assumptions

I am using CDK with TypeScript.

I also use v2 CDK.

❯ cdk --version
2.31.0 (build b67950d)
Enter fullscreen mode Exit fullscreen mode

What is Zod?

It is a TypeScript validation tool, see GitHub.

Zod is a tool that allows you to perform validation based on defined rules at the timing of parsing object information, such as requests, into objects of a defined type.

You can write validation rules in the same file as the type definition, so it is very easy to perform validation.

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>;
Enter fullscreen mode Exit fullscreen mode
// Validation is performed here at the same time.
const request: Request = RequestSchema.parse(body);
Enter fullscreen mode Exit fullscreen mode

As above, not only type, optional (.optional()), and empty character check (.min(1)), but also email format handling (.email()) can be performed.

In case of a validation error, the following error is output.

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

Code Examples

Let's start with a code example.

I will take the three main files as examples.

  • Zod schema
  • Stack class
  • Validator class

If you implement validation with the following code, the defined validation will be performed at cdk synth time.

Zod Schema

First, define the parameters you want to validate as a Zod schema and define the validation rules by Zod for the properties of the schema.

Also, by using z.infer<typeof StackInputSchema>, you can define a type from the schema, which is useful to use as a type when dealing with stack classes and validator classes.

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>;

Enter fullscreen mode Exit fullscreen mode

Stack class

This is a simple example. The init() is used to validate and the create() is used to create the resource.

The StackInput type defined earlier is used to define a variable for validation, which is then passed directly to the constructor of the validator.

export class SampleStack extends Stack {
  private stackInput: StackInput;

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

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

  private init(): void {
    this.stackInput = {
      topicName: props.config.topicName, // Pass parameters from outside the stack via StackProps
      mailAddress: props.config.mailAddress,
    };

    const stackValidator = new StackValidator(this.stackInput);
    this.node.addValidation(stackValidator);
  }

  private create() {
  // ...
Enter fullscreen mode Exit fullscreen mode

In init(), create an instance of the validator class, pass the information to be validated in the constructor, and pass the instance to the method this.node.addValidation.

This is the validation method provided by CDK, which allows multiple validations to be registered, and the validation is run during cdk synth.

Validator class

This is a class to write a concrete validation process, and it performs the validation process with Zod in validate().

By cutting out from the stack class as a validator class like this, if you stop using Zod and change the validation method, you do not have to modify the stack class itself, which has the advantage of making it more maintainable.

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

export class StackValidator implements IValidation {
  private stackInput: StackInput;

  constructor(stackInput: StackInput) {
    this.stackInput = stackInput;
  }

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

    try {
      StackInputSchema.parse(this.stackInput);
    } catch (e) {
      errors.push(JSON.stringify(e));
    }

    return errors;
  }
}
Enter fullscreen mode Exit fullscreen mode

By passing the object to be validated to the parse method of the schema defined in the Zod schema file described below, the parsing and validation are performed together.

If successful, the object will be stored in a variable of the type you also define, and if unsuccessful, a Zod validation error will be thrown.

StackInputSchema.parse(this.stackInput);
Enter fullscreen mode Exit fullscreen mode

Validation run

Now, let's quickly run cdk synth with the email address intentionally formatted improperly (abcde.fgh).

/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
Enter fullscreen mode Exit fullscreen mode

As defined in Zod's schema, it caught the email validation and output an error.


Finally

I wrote an article on validation with CDK last time, and this time I put more focus on TypeScript and included Zod for convenience.

After all, it is easy to leave validation to a framework.

💖 💪 🙅 🚩
k_goto
k.goto

Posted on June 18, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related