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.
Recommended library: Zod
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.
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
orWARNING
log level; - Respond properly: if the validation of an HTTP request failed, always respond with an HTTP 400 (optionally with a Problem response)