decoders

Overview

Elegant and battle-tested validation library for type-safe input data for TypeScript.

Installation

npm install decoders

Make 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() or z.object(). You write object({ 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 unsafe any or unknown values 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 number

For more information, see Error Formatting.

On this page