Skip to content

Schema validation with Zod

Sometimes you need to perform validation on a data structure. Below you can find some best practices on when to use it, and how to implement it.

What about type-safety?

Using TypeScript to your advantage, and adhering to a very strict type-safety discipline is a good practice, and prevents a lot of mistakes and inconsistencies in your code, making advantage of compile-time static code analysis.

In reality however, there are also situations where you cannot be certain of the shape of a certain object. In such cases it is very tempting to perform a type-cast, but this can easily lead to a single-point-of-failure where an unknown data structure is unsafely assumed to be a known type.

This can lead to unexpected failures, or even incorrect data being processed, persisted or forwarded elsewhere.

A better way is to mark such objects with the unknown type, and perform a runtime validation that, at the same time, can also act as type-guard.

When is runtime validation needed?

A very typical case where validation is needed is when you are parsing JSON:

  • ... in an AWS Lambda handler (e.g. when using JSON.parse())
  • ... in a REST client (e.g. when using Fetch API, XMLHttpRequest or a library like Axios)
  • ... from another source, e.g. reading a file, or a payload from a message queue

Best practice

When consuming JSON from a source that cannot be trusted, we recommend to use the unknown type, and perform a dedicated validation step to assert the structure of the parsed object.

An example of an untrusted sources is user input, but data from a remote service may also need validation, since a contract can always be broken.

To enable a schema-based validation of JSON structures, we recommend to use Zod. This library enables a very elegant way to define a validation schema with built-in type-safety.

typescript
import { z } from "zod";

// define your schema...
const Person = z.object({
  name: z.string(),
  age: z.number(),
});

// infers the type definition from the schema object
type Person = z.infer<typeof Person>;

function parsePerson(jsonData: unknown): Person {
  // uses the schema to parse the unknown value into a `Person`
  // will throw an error if the data does not match the schema
  return Person.parse(jsonData);
}

// the returned `Person` object is 100% safe to use
const person = parsePerson(JSON.parse(" ... "));

Handling validation errors

To handle validation errors, you may want to:

  • Fail fast: validate as early as possible, and abort the flow with a descriptive error;
  • Log your errors: on either ERROR or WARNING log level;
  • Respond properly: if the validation of an HTTP request failed, always respond with an HTTP 400 (optionally with a Problem response)