Elegant and battle-tested validation library for type-safe input data for TypeScript and Flow. The API is inspired by Elmβs JSON decoders, hence the name.
See https://nvie.com/posts/introducing-decoders/ for an introduction.
If you're using Flow or TypeScript to statically typecheck your JavaScript, you'll know that any data coming from outside your programβs boundaries is essentially untyped and unsafe. "Decoders" can help to validate and enforce the correct shape of that data.
For example, imagine your app expects a list of points in an incoming HTTP request:
{
points: [
{ x: 1, y: 2 },
{ x: 3, y: 4 },
],
}
In order to decode this, you'll have to tell Flow about the expected structure, and use the decoders to validate at runtime that the free-form data will be in the expected shape.
type Point = { x: number, y: number };
type Payload = {
points: Array<Point>,
};
Here's a decoder that will work for this type:
import { array, guard, number, object } from 'decoders';
const point = object({
x: number,
y: number,
});
const payload = object({
points: array(point),
});
const payloadGuard = guard(payload);
And then, you can use it to decode values:
>>> payloadGuard(1) // throws!
>>> payloadGuard('foo') // throws!
>>> payloadGuard({ // OK!
... points: [
... { x: 1, y: 2 },
... { x: 3, y: 4 },
... ],
... })
At the heart, a decoder is a function that will take any unsafe input, verify it, and either return an "ok" or an annotated "err" result. It will never throw an error when called.
A guard is a convenience wrapper which will use the decoder
The decoders package consists of a few building blocks:
# guard(decoder: Decoder<T>, formatter?: Annotation => string): Guard<T> <>
Turns any given Decoder<T>
into a Guard<T>
.
A guard works like a decoder, but will either:
- Return the decoded value (aka the happy path)
- Or throw an exception
So a Guard bypasses the intermediate "Result" type that decoders output. An "ok" result will get returned, an "err" result will be formatted into an error message and thrown.
The typical usage is that you keep composing decoders until you have one decoder for your entire input object, and then use a guard to wrap that outer decoder. Decoders can be composed to build larger decoders. Guards cannot be composed.
By default, guard()
will use the formatInline
error formatter. You can pass another
built-in formatter as the second argument, or provide your own. (This will require
understanding the internal Annotation
datastructure that decoders uses for error
reporting.)
Built-in formatters are:
-
formatInline
(default) β will echo back the input object and inline error messages smartly. Example:import { array, guard, object, string } from 'decoders'; import { formatInline } from 'decoders/format'; const mydecoder = array(object({ name: string, age: number })); const defaultGuard = guard(mydecoder, formatInline); defaultGuard([{ name: 'Alice', age: '33' }]);
Will throw the following error message:
Decoding error: [ { name: 'Alice', age: '33', ^^^^ Must be number }, ]
-
formatShort
β will report the path into the object where the error happened. Example:import { formatShort } from 'decoders/format'; const customGuard = guard(mydecoder, formatShort);
Will throw the following error message:
Decoding error: Value at keypath 0.age: Must be number
Accepts only finite numbers (integer or float values). This means that values like NaN
,
or positive and negative Infinity
are not considered valid numbers.
const verify = guard(number);
// π
verify(123) === 123;
verify(-3.14) === -3.14;
// π
verify(Infinity); // throws
verify(NaN); // throws
verify('not a number'); // throws
# integer: Decoder<integer> <>
Like number
, but only accepts values that are whole numbers.
const verify = guard(integer);
// π
verify(123) === 123;
// π
verify(-3.14); // throws
verify(Infinity); // throws
verify(NaN); // throws
verify('not a integer'); // throws
# positiveNumber: Decoder<number> <>
Accepts only positive finite numbers (integer or float values).
const verify = guard(positiveNumber);
// π
verify(123) === 123;
// π
verify(-42); // throws
verify(3.14); // throws
verify(Infinity); // throws
verify(NaN); // throws
verify('not a number'); // throws
# positiveInteger: Decoder<number> <>
Accepts only positive finite integers.
const verify = guard(positiveInteger);
// π
verify(123) === 123;
// π
verify(-3); // throws
verify(3.14); // throws
verify(Infinity); // throws
verify(NaN); // throws
verify('not a number'); // throws
Accepts only string values.
const verify = guard(string);
// π
verify('hello world') === 'hello world';
verify('π') === 'π';
verify('') === '';
// π
verify(123); // throws
verify(true); // throws
verify(null); // throws
# nonEmptyString: Decoder<string> <>
Like string
, but will reject the empty string, or strings containing only whitespace.
const verify = guard(nonEmptyString);
// π
verify('hello world') === 'hello world';
verify('π') === 'π';
// π
verify(123); // throws
verify(' '); // throws
verify(''); // throws
Accepts only string values that match the given regular expression.
const verify = guard(regex(/^[0-9][0-9]+$/));
// π
verify('42') === '42';
verify('83401648364738') === '83401648364738';
// π
verify(''); // throws
verify('1'); // throws
verify('foo'); // throws
Accepts only strings that are syntactically valid email addresses. (This will not mean that the email address actually exist.)
const verify = guard(email);
// π
verify('[email protected]') === '[email protected]';
// π
verify('foo'); // throws
verify('@acme.org'); // throws
verify('alice @ acme.org'); // throws
Accepts strings that are valid URLs, returns the value as a URL instance.
const verify = guard(url);
// π
verify('http://nvie.com') === new URL('http://nvie.com/');
verify('https://nvie.com') === new URL('https://nvie.com/');
verify('git+ssh://[email protected]/foo/bar.git') === new URL('git+ssh://[email protected]/foo/bar.git');
// π
verify('foo'); // throws
verify('@acme.org'); // throws
verify('alice @ acme.org'); // throws
verify('/search?q=foo'); // throws
Accepts strings that are valid URLs, but only HTTPS ones. Returns the value as a URL instance.
const verify = guard(httpsUrl);
// π
verify('https://nvie.com:443') === new URL('https://nvie.com/');
// π
verify('http://nvie.com'); // throws, not HTTPS
verify('git+ssh://[email protected]/foo/bar.git'); // throws, not HTTPS
Tip! If you need to limit URLs to different protocols than HTTP, you can do as the
HTTPS decoder is implemented: as a predicate on top of a regular url
decoder.
import { predicate, url } from 'decoders';
const gitUrl: Decoder<URL> = predicate(
url,
(value) => value.protocol === 'git:',
'Must be a git:// URL',
);
# boolean: Decoder<boolean> <>
Accepts only boolean values.
const verify = guard(boolean);
// π
verify(false) === false;
verify(true) === true;
// π
verify(undefined); // throws
verify('hello world'); // throws
verify(123); // throws
Accepts any value and will return its "truth" value. Will never reject.
const verify = guard(truthy);
// π
verify(false) === false;
verify(true) === true;
verify(undefined) === false;
verify('hello world') === true;
verify('false') === true;
verify(0) === false;
verify(1) === true;
verify(null) === false;
// π
// This decoder will never reject an input
# numericBoolean: Decoder<boolean> <>
Accepts only number values, but return their boolean representation.
const verify = guard(numericBoolean);
// π
verify(-1) === true;
verify(0) === false;
verify(123) === true;
// π
verify(false); // throws
verify(true); // throws
verify(undefined); // throws
verify('hello'); // throws
Accepts only JavaScript Date values.
const verify = guard(date);
const now = new Date();
// π
verify(now) === now;
// π
verify(123); // throws
verify('hello'); // throws
Accepts only ISO8601-formatted strings. This is very useful for working
with dates in APIs: serialize them as .toISOString()
when sending, decode them with
iso8601
when receiving.
NOTE: This decoder accepts strings, but returns Date instances.
const verify = guard(iso8601);
// π
verify('2020-06-01T12:00:00Z'); // β new Date('2020-06-01T12:00:00Z')
// π
verify('2020-06-01'); // throws
verify('hello'); // throws
verify(123); // throws
verify(new Date()); // throws (does not accept dates)
Accepts only the literal null
value.
const verify = guard(null_);
// π
verify(null) === null;
// π
verify(false); // throws
verify(undefined); // throws
verify('hello world'); // throws
# undefined_: Decoder<undefined> <>
Accepts only the literal undefined
value.
const verify = guard(undefined_);
// π
verify(undefined) === undefined;
// π
verify(null); // throws
verify(false); // throws
verify('hello world'); // throws
# constant<T>(value: T): Decoder<T> <>
Accepts only the given constant value.
For TypeScript, use this syntax:
constant('something' as const);
constant(42 as const);
For Flow, use this syntax:
constant(('something': 'something'));
constant((42: 42));
Example:
const verify = guard(constant('hello' as const));
// π
verify('hello') === 'hello';
// π
verify('this breaks'); // throws
verify(false); // throws
verify(undefined); // throws
# hardcoded<T>(value: T): Decoder<T> <>
Accepts any input, completely ignores it, and always returns the provided value. This is useful to manually add extra fields to object decoders.
const verify = guard(hardcoded(42));
// π
verify('hello') === 42;
verify(false) === 42;
verify(undefined) === 42;
// π
// This decoder will never reject an input
Rejects all inputs, and always fails with the given error message. May be useful for explicitly disallowing keys, or for testing purposes.
const verify = guard(object({
a: string,
b: optional(fail('Key b has been removed')),
}));
// π
verify({ a: 'foo' }); // β { a: 'foo' };
verify({ a: 'foo', c: 'bar' }); // β { a: 'foo' };
// π
verify({ a: 'foo', b: 'bar' }); // throws
# unknown: Decoder<unknown>
<>
# mixed: Decoder<mixed>
<>
Accepts any value and returns it unchanged. Useful for situation in which you don't know or expect a specific type. Of course, the downside is that you won't know the type of the value statically and you'll have to further refine it yourself.
const verify = guard(mixed);
// π
verify('hello') === 'hello';
verify(false) === false;
verify(undefined) === undefined;
verify([1, 2]) === [1, 2];
// π
// This decoder will never reject an input
Composite decoders can build new decoders from existing decoders that can already decode a
"subtype". Examples are: if you already have a string
decoder (of type
Decoder<string>
), then you can use array(string)
to automatically build a decoder for
arrays of strings, which will be of type Decoder<Array<string>>
.
# optional<T>(Decoder<T>): Decoder<T | undefined> <>
Accepts only the literal value undefined
, or whatever the given decoder accepts.
const verify = guard(optional(string));
// π
verify('hello') === 'hello';
verify(undefined) === undefined;
// π
verify(null); // throws
verify(0); // throws
verify(42); // throws
A typical case where optional
is useful is in decoding objects with optional fields:
object({
id: number,
name: string,
address: optional(string),
});
Which will decode to type:
{
id: number,
name: string,
address?: string,
}
# nullable<T>(Decoder<T>): Decoder<T | null> <>
Accepts only the literal value null
, or whatever the given decoder accepts.
const verify = guard(nullable(string));
// π
verify('hello') === 'hello';
verify(null) === null;
// π
verify(undefined); // throws
verify(0); // throws
verify(42); // throws
# maybe<T>(Decoder<T>): Decoder<?T> <>
Accepts only undefined
, null
, or whatever the given decoder accepts.
const verify = guard(maybe(string));
// π
verify('hello') === 'hello';
verify(null) === null;
verify(undefined) === undefined;
// π
verify(0); // throws
verify(42); // throws
# array<T>(Decoder<T>): Decoder<Array<T>> <>
Accepts only arrays of whatever the given decoder accepts.
const verify = guard(array(string));
// π
verify(['hello', 'world']) === ['hello', 'world'];
// π
verify(['hello', 1.2]); // throws
# nonEmptyArray<T>(Decoder<T>): Decoder<Array<T>> <>
Like array()
, but will reject arrays with 0 elements.
const verify = guard(nonEmptyArray(string));
// π
verify(['hello', 'world']) === ['hello', 'world'];
// π
verify(['hello', 1.2]); // throws
verify([]); // throws
# poja: Decoder<Array<unknown>> <>
Accepts any array, but doesn't validate its items further.
"poja" means "plain old JavaScript array", a play on "pojo".
const verify = guard(poja);
// π
verify([1, 'hi', true]) === [1, 'hi', true];
verify(['hello', 'world']) === ['hello', 'world'];
verify([]) === [];
// π
verify({}); // throws
verify('hi'); // throws
# tuple<A, B, C, ...>(Decoder<A>, Decoder<B>, Decoder<C>): Decoder<[A, B, C, ...]> <>
Accepts a tuple (an array with exactly n items) of values accepted by the n given decoders.
const verify = guard(tuple(string, number));
// π
verify(['hello', 1.2]) === ['hello', 1.2];
// π
verify([]); // throws, too few items
verify(['hello', 'world']); // throws, not the right types
verify(['a', 1, 'c']); // throws, too many items
# set<T>(Decoder<T>): Decoder<Set<T>> <>
Similar to array
, but returns the result as an ES6 Set.
const verify = guard(set(string));
// π
verify(['hello', 'world']) // β new Set(['hello', 'world']);
// π
verify(['hello', 1.2]); // throws
# object<O: { [field: string]: Decoder<any> }>(mapping: O): Decoder<{ ... }> <>
Accepts object values with fields matching the given decoders. Extra fields that exist on the input object are ignored and will not be returned.
const verify = guard(
object({
x: number,
y: number,
}),
);
// π
verify({ x: 1, y: 2 }) === { x: 1, y: 2 };
verify({ x: 1, y: 2, z: 3 }) === { x: 1, y: 2 }; // β οΈ extra field `z` not returned!
// π
verify({ x: 1 }); // throws, missing field `y`
For more information, see also
The difference between object
, exact
, and inexact
.
# exact<O: { [field: string]: Decoder<any> }>(mapping: O): Decoder<{ ... }> <>
Like object()
, but will reject inputs that contain extra keys that are not specified
explicitly.
const verify = guard(
exact({
x: number,
y: number,
}),
);
// π
verify({ x: 1, y: 2 }) === { x: 1, y: 2 };
// π
verify({ x: 1, y: 2, z: 3 }); // throws, extra field `z` not allowed
verify({ x: 1 }); // throws, missing field `y`
For more information, see also
The difference between object
, exact
, and inexact
.
# inexact<O: { [field: string]: Decoder<any> }>(mapping: O): Decoder<{ ... }> <>
Like object()
, but will pass through any extra fields on the input object unvalidated
that will thus be of unknown
type statically.
const verify = guard(
inexact({
x: number,
}),
);
// π
verify({ x: 1, y: 2 }) === { x: 1, y: 2 };
verify({ x: 1, y: 2, z: 3 }) === { x: 1, y: 2, z: 3 };
// π
verify({ x: 1 }); // throws, missing field `y`
For more information, see also
The difference between object
, exact
, and inexact
.
# pojo: Decoder<{ [key: string]: unknown }> <>
Accepts any "plain old JavaScript object", but doesn't validate its keys or values further.
const verify = guard(pojo);
// π
verify({}) === {};
verify({ name: 'hi' }) === { name: 'hi' };
// π
verify('hi'); // throws
verify([]); // throws
verify(new Date()); // throws
verify(null); // throws
# dict<T>(Decoder<T>): Decoder<{ [string]: <T>}> <>
Accepts objects where all values match the given decoder, and returns the result as a
{ [string]: T }
.
The main difference between object()
and dict()
is that you'd typically use object()
if this is a record-like object, where all field names are known and the values are
heterogeneous. Whereas with dict()
the keys are typically dynamic and the values
homogeneous, like in a dictionary, a lookup table, or a cache.
const verify = guard(dict(person)); // Assume you have a "person" decoder already
// π
verify({
1: { name: 'Alice' },
2: { name: 'Bob' },
3: { name: 'Charlie' },
}); // β {
// "1": { name: "Alice" },
// "2": { name: "Bob" },
// "3": { name: "Charlie" },
// }
# mapping<T>(Decoder<T>): Decoder<Map<string, T>> <>
Like dict()
, but returns the result as a Map<string, T>
instead.
const verify = guard(mapping(person)); // Assume you have a "person" decoder already
// π
verify({
1: { name: 'Alice' },
2: { name: 'Bob' },
3: { name: 'Charlie' },
});
// β Map([
// ['1', { name: 'Alice' }],
// ['2', { name: 'Bob' }],
// ['3', { name: 'Charlie' }],
// ]);
Accepts any value that's a valid JSON value:
null
string
number
boolean
{ [string]: JSONValue }
Array<JSONValue>
const verify = guard(json);
// π
verify({
name: 'Amir',
age: 27,
admin: true,
image: null,
tags: ['vip', 'staff'],
});
Any value returned by JSON.parse()
should decode without failure.
# jsonObject: Decoder<JSONObject> <>
Like json
, but will only decode when the JSON value is an object.
const verify = guard(json);
// π
verify({}); // β {}
verify({ name: 'Amir' }); // β { name: 'Amir' }
// π
verify([]); // throws
verify([{ name: 'Alice' }]); // throws
verify('hello'); // throws
verify(null); // throws
# jsonArray: Decoder<JSONArray> <>
Like json
, but will only decode when the JSON value is an array.
const verify = guard(json);
// π
verify([]); // β []
verify([{ name: 'Amir' }]); // β [{ name: 'Amir' }]
// π
verify({}); // throws
verify({ name: 'Alice' }); // throws
verify('hello'); // throws
verify(null); // throws
# either<A, B, C,
...>(Decoder<A>, Decoder<B>, Decoder<C>,
...): Decoder<A | B | C | ...>
<>
Accepts any value that is accepted by any of the given decoders. The decoders are attempted on the input in given order. The first one that accepts the input "wins".
const verify = guard(either(number, string));
// π
verify('hello world') === 'hello world';
verify(123) === 123;
// π
verify(false); // throws
NOTE to Flow users: In Flow, there is a max of 16 arguments with this construct. (This
is no problem in TypeScript.) If you hit the 16 argument limit, you can work around that
by stacking, e.g. do either(<15 arguments here>, either(...))
.
# disjointUnion<O: { [field: string]: (Decoder<T> | Decoder<V> | ...) }>(field: string, mapping: O): Decoder<T | V | ...> <>
NOTE: In [email protected], this was called dispatch()
.
Like either
, but only for building unions of object types with a common field (like a
type
field) that lets you distinguish members.
The following two decoders are effectively equivalent:
type Rect = { __type: 'rect', x: number, y: number, width: number, height: number };
type Circle = { __type: 'circle', cx: number, cy: number, r: number };
// ^^^^^^
// Field that defines which decoder to pick
// vvvvvv
const shape1: Decoder<Rect | Circle> = disjointUnion('__type', { rect, circle });
const shape2: Decoder<Rect | Circle> = either(rect, circle);
But using disjointUnion()
will typically be more runtime-efficient than using
either()
. The reason is that disjointUnion()
will first do minimal work to "look
ahead" into the type
field here, and based on that value, pick which decoder to invoke.
Error messages will then also be tailored to the specific decoder.
The either()
version will instead try each decoder in turn until it finds one that
matches. If none of the alternatives match, it needs to report all errors, which is
sometimes confusing.
# oneOf<T>(Array<T>):
Decoder<T>
<>
Accepts any value that is strictly-equal (using ===
) to one of the specified values.
const verify = guard(oneOf(['foo', 'bar', 3]));
// π
verify('foo') === 'foo';
verify(3) === 3;
// π
verify('hello'); // throws
verify(4); // throws
verify(false); // throws
For example, given an array of strings, like so:
oneOf(['foo', 'bar']);
TypeScript is capable of inferring the return type as Decoder<'foo' | 'bar'>
, but in
Flow it will (unfortunately) be Decoder<string>
. So in Flow, be sure to explicitly
annotate the type. Either by doing oneOf([('foo': 'foo'), ('bar': 'bar')])
, or as
oneOf<'foo' | 'bar'>(['foo', 'bar'])
.
#
instanceOf<T>(Class<T>): Decoder<T>
<>
Accepts any value that is an instanceof
the given class.
NOTE: Help wanted! The TypeScript annotation for this decoder needs help! If you know how to express it, please submit a PR. See https://github.com/nvie/decoders/blob/main/src/core/instanceOf.d.ts
const verify = guard(instanceOf(Error));
// π
const value = new Error('foo');
verify(value) === value;
// π
verify('foo'); // throws
verify(3); // throws
# transform<T,
V>(Decoder<T>, <T> => <V>):
Decoder<V>
<>
Accepts any value the given decoder accepts, and on success, will call the mapper value on the decoded result. If the mapper function throws an error, the whole decoder will fail using the error message as the failure reason.
const upper = transform(string, (s) => s.toUpperCase());
const verify = guard(upper);
// π
verify('foo') === 'FOO';
// π
verify(4); // throws
# compose<T,
V>(Decoder<T>, Decoder<V, T>): Decoder<V>
<>
Given a decoder for T and another one for V-given-a-T. Will first decode the input
using the first decoder, and if okay, pass the result on to the second decoder. The
second decoder will thus be able to make more assumptions about its input value, i.e. it
can know what type the input value is (T
instead of unknown
).
This is an advanced decoder, typically only useful for authors of decoders. It's not recommended to rely on this decoder directly for normal usage.
#
predicate<T>(Decoder<T>, <T> => boolean,
string): Decoder<T>
<>
Accepts values that are accepted by the decoder and also pass the predicate function.
const odd = predicate(
number,
(n) => n % 2 !== 0,
'Must be odd'
);
const verify = guard(odd);
// π
verify(3) === 3;
// π
verify('hi'); // throws: not a number
verify(42); // throws: not an odd number
In TypeScript, if you provide a predicate that also doubles as a type predicate, then this will be reflected in the return type, too.
# prep<T, I>(unknown => I,
Decoder<T, I>): Decoder<T>
<>
Pre-process the raw data input before passing it into the decoder. This gives you the
ability to arbitrarily customize the input on the fly before passing it to the decoder. Of
course, the input value at that point is still of unknown
type, so you will have to deal
with that accordingly.
const verify = prep(
// Will convert any input to an int first, before feeding it to
// positiveInteger. This will effectively also allow numeric strings
// to be accepted (and returned) as integers. If this ever throws,
// then the error message will be what gets annotated on the input.
x => parseInt(x),
positiveInteger,
);
// π
verify(42) === 42;
verify('3') === 3;
// π
verify('-3'); // throws: not a positive number
verify('hi'); // throws: not a number
#
describe<T>(Decoder<T>, string):
Decoder<T>
<>
Uses the given decoder, but will use an alternative error message in case it rejects. This can be used to simplify or shorten otherwise long or low-level/technical errors.
const vowel = describe(
either5(constant('a'), constant('e'), constant('i'), constant('o'), constant('u')),
'Must be vowel',
);
# lazy<T>(() =>
Decoder<T>): Decoder<T>
<>
Lazily evaluate the given decoder. This is useful to build self-referential types for recursive data structures. Example:
type Tree = {
value: string,
children: Array<Tree>,
// ^^^^
// Self-reference defining a recursive type
};
const treeDecoder: Decoder<Tree> = object({
value: string,
children: array(lazy(() => treeDecoder)),
// ^^^^^^^^^^^^^^^^^^^^^^^
// Use lazy() like this to refer to the treeDecoder which is
// getting defined here
});
The three decoders in the "object" family of decoders only differ in how they treat extra properties on input values.
For example, for a definition like:
import { exact, inexact, number, object, string } from 'decoders';
const thing = {
a: string,
b: number,
};
And a runtime input of:
{
a: "hi",
b: 42,
c: "extra", // Note "c" is not a known field
}
Extra properties | Output value | Inferred type | |
---|---|---|---|
object(thing) |
discarded | {a: "hi", b: 42} |
{a: string, b: number} |
exact(thing) |
not allowed | β‘οΈ Runtime error | {a: string, b: number} |
inexact(thing) |
retained | {a: "hi", b: 42, c: "extra"} |
{a: string, b: number, [string]: unknown} |
There are two main building blocks for defining your own custom decoders: transform()
and compose()
.
There are roughly 3 use cases that you will want to use:
- Transformation (i.e. read one type, but return another, or read a type but change its value before returning)
- Adding extra value requirements (i.e. decode using an existing decoder, but require an extra value check)
- Chaining multiple decoders (less common, more advanced)
To read one type from the input, but return another, use:
const numericString: Decoder<number> = transform(
// At runtime, expect to read a string...
string,
// ...but return it as a number
(s) => Number(s),
);
To read one type, but change its value before returning:
const upperCase: Decoder<string> = transform(string, (s) => s.toUpperCase());
WARNING: While you can map anything to anything, it's typically NOT A GOOD IDEA to put too much transformation logic inside decoders. It's recommended to keep them minimal and only try to use them for the most basic use cases, like in the examples above. Keeping business logic outside decoders makes them more reusable and composable.
The easiest way to decode using an existing decoder, but enforcing extra runtime checks on
their values is by wrapping it in a predicate(...)
construction:
const odd = predicate(integer, (n) => n % 2 !== 0, 'Must be odd');
const shortString = predicate(string, (s) => s.length < 8, 'Must be less than 8 chars');