Overview
Elegant and battle-tested validation library for type-safe input data for TypeScript.
Installation
npm install decodersMake sure you have strict: true in your tsconfig.json for type inference to work
correctly.
Getting started
Let’s look at a simple decoder to validate user data.
Define your shape
import { email, number, object, optional, string } from 'decoders';
const userDecoder = object({
id: number,
name: string,
email: optional(email),
});Validate inputs at runtime
function handleRequest(body: unknown) {
const user = userDecoder.verify(body);
// If we get here, user is guaranteed to be of the expected shape
return saveUser(user);
}You get full type inference...
const user = userDecoder.verify(body);
// ^^^^
// TypeScript will infer this type as:
// {
// id: number;
// name: string;
// email?: string | undefined;
// }...or a beautiful runtime error
If verification fails, you will see a clear error message pointing out the problem.
Decoding error:
{
id: 123,
name: "Alison",
email: "not an email",
^^^^^^^^^^^^^^ Must be email
}Why decoders?
Decoders is one of the OG runtime validation libraries for TypeScript, in production since 2017. Here's what makes it different:
- Tiny footprint — Zero external dependencies, fully tree-shakeable, only ~4 KB gzipped (if you'd use everything).
- Runs everywhere — Node.js, browsers, Cloudflare Workers, Bun, etc.
- Reads like a type definition — No
z.string()orz.object(). You writeobject({ name: string, age: number })and it looks almost identical to the TypeScript type it produces. - Best-in-class error messages — When validation fails, decoders echoes back your actual input data with annotations inlined exactly where and why validation failed. Optimized for human readability.
- Standard Schema compliant — Works out of the box with any framework that supports Standard Schema.
When to use decoders
Use decoders whenever you need to validate untrusted data. Typically at the boundaries of your application.
- API responses - Validate data from external APIs
- User input - Validate form submissions and user-provided data
- Configuration files - Validate JSON/YAML configuration
- Database queries - Validate data from databases
- Message queues - Validate messages from queues
- Environment variables - Validate and parse environment configuration
- File uploads - Validate uploaded file contents
- Eliminating
any- Turn unsafeanyorunknownvalues into fully typed data
Composing decoders
Decoders compose together like LEGO® blocks to build larger ones. In the example above,
string, number, and isoDate are all individual decoders that get
combined with object, array, and optional.
Every decoder in this library can be used standalone or composed with others. You can also build your own to match any shape of data.
const zipCode = regex(/^\d{5}$/, 'Must be 5-digit zip code');
const addressDecoder = object({
street: string,
city: string,
zip: zipCode,
});
const userDecoder = object({
name: nonEmptyString,
email: email,
address: addressDecoder,
roles: array(oneOf(['admin', 'editor', 'viewer'])),
});Error formatting
By default, .verify() uses formatInline, which echoes back the input with errors
inlined:
import { object, string, number } from 'decoders';
const decoder = object({
name: string,
age: number,
});
decoder.verify({ name: 'Alison', age: '33' });
// Decoding error:
// {
// name: "Alison",
// age: "33",
// ^^^^ Must be number
// }You can also use formatShort for a compact single-line format:
import { formatShort } from 'decoders';
decoder.verify({ name: 'Alison', age: '33' }, formatShort);
// Decoding error: Value at key 'age': Must be numberFor more information, see Error Formatting.