TypeScript type annotations provide compile-time guarantees. However, when data flows into our clients from external sources, many things can go wrong at runtime.
JSON decoders validate our JSON before it enters our program. This way, if the data has an unexpected structure, we're immediately alerted.
If you are new to JSON decoding, you may want to read the introductory article Decoding JSON with Typescript about why and how to use this library.
npm install ts.data.json --save
type User = {
firstname: string;
lastname: string;
};
const userDecoder = JsonDecoder.object<User>(
{
firstname: JsonDecoder.string,
lastname: JsonDecoder.string
},
'User'
);
const jsonObjectOk = {
firstname: 'Damien',
lastname: 'Jurado'
};
userDecoder
.decodeToPromise(jsonObjectOk)
.then(user => {
console.log(`User ${user.firstname} ${user.lastname} decoded successfully`);
})
.catch(error => {
console.log(error);
});
// Output: User Damien Jurado decoded successfully
const jsonObjectKo = {
firstname: 'Erik',
lastname: null
};
userDecoder
.decodeToPromise(jsonObjectKo)
.then(user => {
console.log('User decoded successfully');
})
.catch(error => {
console.error(error);
});
// Output: <User> decoder failed at key "lastname" with error: null is not a valid string
Alternatively, you can use FromDecoder<D>
to infer your types based on your decoder definitions:
const userDecoder = JsonDecoder.object(
{
firstname: JsonDecoder.string,
lastname: JsonDecoder.string
},
'User'
);
type User = FromDecoder<typeof userDecoder>; // no need to declare the User interface!
decode(json: any): Result<a>
Decodes a JSON object of type <a>
and returns a Result<a>
.
The JSON object to decode.
JsonDecoder.string.decode('hi'); // Ok<string>({value: 'hi'})
JsonDecoder.string.decode(5); // Err({error: '5 is not a valid string'})
fold<b>( onOk: (result: a) => b, onErr: (error: string) => b, json: any ): b
Decodes a JSON object of type <a>
and calls onOk()
on success or onErr()
on failure, both return <b>
.
Function called when the decoder succeeds.
Function called when the decoder fails.
The JSON object to decode.
JsonDecoder.string.fold(
(value: string) => parseInt(value, 10),
(error: string) => 0,
'000000000001'
); // 1
decodeToPromise(json: any): Promise<a>
Decodes a JSON object of type <a>
and returns a Promise<a>
The JSON object to decode.
JsonDecoder.string.decodeToPromise('hola').then(res => console.log(res)); // 'hola'
JsonDecoder.string.decodeToPromise(2).catch(err => console.log(err)); // '2 is not a valid string'
map<b>(fn: (value: a) => b): Decoder<b>
If the decoder has succeeded, transform the decoded value into something else, otherwise nothing will happen.
The transformation function.
// Decode a string, then transform it into a Date
const dateDecoder = JsonDecoder.string.map(stringDate => new Date(stringDate));
// Ok scenario
dateDecoder.decode('2018-12-21T18:22:25.490Z'); // Ok<Date>({value: Date(......)})
// Err scenario
dateDecoder.decode(false); // Err({error: 'false is not a valid string'})
chain<b>(fn: (value: a) => Decoder<b>): Decoder<b>
Chain decoders that might fail.
The chain function.
const adultDecoder = JsonDecoder.number.chain(age =>
age >= 18
? JsonDecoder.succeed
: JsonDecoder.fail(`Age ${age} is less than 18`)
);
adultDecoder.decode(18); // Ok<number>({value: 18})
adultDecoder.decode(17); // Err({error: 'Age 17 is less than 18'})
string: Decoder<string>
Creates a string
decoder.
JsonDecoder.string.decode('hi'); // Ok<string>({value: 'hi'})
JsonDecoder.string.decode(5); // Err({error: '5 is not a valid string'})
number: Decoder<number>
Creates a number
decoder.
JsonDecoder.number.decode(99); // Ok<number>({value: 99})
JsonDecoder.number.decode('hola'); // Err({error: 'hola is not a valid number'})
boolean: Decoder<boolean>
Creates a boolean
decoder.
JsonDecoder.boolean.decode(true); // Ok<boolean>({value: true})
JsonDecoder.boolean.decode(null); // Err({error: 'null is not a valid boolean'})
emptyObject: Decoder<{}>
Creates a {}
decoder.
JsonDecoder.emptyObject.decode({}); // Ok<{}>({value: {}})
JsonDecoder.emptyObject.decode({ a: 1 }); // Err({error: '{a:1} is not a valid empty object'})
object<a>(decoders: DecoderObject<a>, decoderName: string, keyMap?: DecoderObjectKeyMap<a>): Decoder<a>
Creates an object
decoder.
Key/value pair that has to comply with the <a>
type.
Turns all optional keys to required, so you have to specify decoders even for the optional (i.e. with
{name?: string}
) keys.
Type of the object we are decoding. i.e. User
. It is used to generate meaningful decoding error messages.
Optional key/value pair to map JSON-land keys with Model-land keys. Useful when the JSON keys don't match with the decoded type keys.
type User = {
firstname: string;
lastname: string;
};
const userDecoder = JsonDecoder.object<User>(
{
firstname: JsonDecoder.string,
lastname: JsonDecoder.string
},
'User'
);
const jsonOk = {
firstname: 'Damien',
lastname: 'Jurado'
};
userDecoder.decode(jsonOk);
// Output: Ok<User>({value: {firstname: 'Damien', lastname: 'Jurado'}})
const jsonKo = {
firstname: null,
lastname: 'Satie'
};
userDecoder.decode(jsonKo);
// Output: Err({error: '<User> decoder failed at key "firstname" with error: null is not a valid string'})
const userDecoder = JsonDecoder.object<User>(
{
firstname: JsonDecoder.string,
lastname: JsonDecoder.string
},
'User',
{
firstname: 'fName',
lastname: 'lName'
}
);
const jsonOk = {
fName: 'Nick',
lName: 'Drake'
};
userDecoder.decode(json);
// Output: Ok({value: {firstname: 'Nick', lastname: 'Drake'}})
const jsonKo = {
fName: 'Nick'
};
userDecoder.decode(json);
// Output: Err({error: '<User> decoder failed at key "lastname" (mapped from the JSON key "lName") with error: undefined is not a valid string'})
objectStrict<a>(decoders: DecoderObject<a>, decoderName: string): Decoder<a>
Creates an object
decoder that performs strict key checks. It only accepts json objects with exactly the same keys as the decoder keys.
Key/value pair that has to comply with the <a>
type.
Type of the object we are decoding. i.e. User
. It is used to generate meaningful decoding error messages.
type User = {
firstname: string;
lastname: string;
};
const userDecoder = JsonDecoder.objectStrict<User>(
{
firstname: JsonDecoder.string,
lastname: JsonDecoder.string
},
'User'
);
const jsonOk = {
firstname: 'Damien',
lastname: 'Jurado'
};
userDecoder.decode(jsonOk);
// Output: Ok<User>({value: {firstname: 'Damien', lastname: 'Jurado'}})
const jsonKo = {
firstname: 'Damien',
lastname: 'Jurado',
email: 'damien@damienjurado.com'
};
userDecoder.decode(jsonKo);
// Output: Err({error: 'Unknown key "email" found while processing strict <User> decoder'})
array<a>(decoder: Decoder<a>, decoderName: string): Decoder<Array<a>>
Creates an array
decoder.
The decoder used to decode every Array<a>
item.
Type of the object we are decoding. i.e. User[]
. It is used to generate meaningful decoding error messages.
JsonDecoder.array<number>(JsonDecoder.number, 'number[]').decode([1, 2, 3]);
// Output: Ok<number[]>({value: [1, 2, 3]})
JsonDecoder.array<number>(JsonDecoder.number, 'number[]').decode([1, '2', 3]);
// Output: Err({error: '<number[]> decoder failed at index 1 with error: "2" is not a valid number'})
dictionary<a>(decoder: Decoder<a>, decoderName: string): Decoder<{ [name: string]: a }>
Creates a dictionary
decoder.
The decoder used to decode every value of the key/value pairs.
Type of the object we are decoding. i.e. User
. It is used to generate meaningful decoding error messages.
JsonDecoder.dictionary(JsonDecoder.number, 'Dict<number>').decode({
a: 1,
b: 2
});
// Output: Ok<Dict<number>>({value: {a: 1, b: 2}})
JsonDecoder.dictionary(JsonDecoder.number, 'Dict<number>').decode({
a: 1,
b: 2,
c: null
});
// Output: Err({error: '<Dict<number>> dictionary decoder failed at key "c" with error: null is not a valid number'})
oneOf<a>(decoders: Array<Decoder<a>>, decoderName: string): Decoder<a>
The oneOf
decoder tries to decode the provided JSON with any of the provided decoders. It returns Ok
with the first successful decoded value or Err
if all decoders fail.
The Array of decoders the JSON can be decoded with.
Type of the object we are decoding. i.e. number | string
. It is used to generate meaningful decoding error messages.
JsonDecoder.oneOf<string | number>(
[JsonDecoder.string, JsonDecoder.number],
'string | number'
).decode(1);
// Output: Ok<string | number>({value: 1})
JsonDecoder.oneOf<string | number>(
[JsonDecoder.string, JsonDecoder.number],
'string | number'
).decode(true);
// Output: Err({error: "<string | number> decoder failed because true can't be decoded with any of the provided oneOf decoders"})
allOf<T extends Array<Decoder<unknown>>, R = AllOfDecoderReturn<T>>(decoders: T): Decoder<R>
The allOf
decoder tries to decode the provided JSON with all of the provided decoders, in order. The output of one decoder is passed as input to the next decoder. It returns Ok
with the last successful decoded value or Err
if any decoder fails.
The allOf
decoder allows you to combine multiple decoders. It is probably most useful when combined with custom decoders you may make for your application.
An array of decoders the JSON should be decoded with.
Simple examples:
JsonDecoder.allOf(
JsonDecoder.string,
JsonDecoder.failover(10, JsonDecoder.number)
).decode('hola'),
// Output: Ok({value: 10})
JsonDecoder.allOf(
JsonDecoder.string,
JsonDecoder.failover(10, JsonDecoder.number)
).decode(5),
// Output: Err({error: "5 is not a valid string})
Example using a custom hasLength()
decoder (an example here):
JsonDecoder.allOf(
JsonDecoder.array(JsonDecoder.number, 'latLang'),
hasLength<[number, number]>(2)
).decode([-123.34324, 23.454365]);
// Output: Ok({value: [-123.34324, 23.454365]})
JsonDecoder.allOf(
JsonDecoder.array(JsonDecoder.number, 'latLang'),
hasLength<[number, number]>(2)
).decode([1, 2, 3]);
// Output: Err({error: "hasLength() decoder failed because the provided array is of length 3."})
tuple(decoders: Decoder[], decoderName: string): Decoder
Creates a tuple
decoder.
An array containing a decoder for each element of the tuple.
Type of the object we are decoding. i.e. [number, string]
. It is used to generate meaningful decoding error messages.
decoder: Decoder<[number, string]> = JsonDecoder.tuple(
[JsonDecoder.number, JsonDecoder.string], '[number, string]');
decoder.decode([1, "foo"]);
// Output: Ok<[number, string]>([1, "foo"])
decoder.decode([1, "foo", 2]);
// Output: Err({error: '<[number, string]> tuple decoder failed because it received a tuple of length 3, but 2 decoders.
enumeration<e>(enumObj: object, decoderName: string): Decoder<e>
Creates a decoder for a (non-const) enum.
The enum object to use for decoding. This doesn't exist for const enums.
Type of the object we are decoding. i.e. User
. It is used to generate meaningful decoding error messages.
enum ExampleEnum {
X = 1,
Y /* 2 */,
Z = 'foo'
}
const exampleEnumDecoder = JsonDecoder.enumeration<ExampleEnum>(
ExampleEnum,
'ExampleEnum'
);
exampleEnumDecoder.decode(1); // Ok<ExampleEnum>({value: 1})
exampleEnumDecoder.decode(ExampleEnum.Y); // Ok<ExampleEnum>({value: 2})
exampleEnumDecoder.decode(3); // Err({error: '<ExampleEnum> decoder failed at value "3" which is not in the enum'})
lazy<a>(mkDecoder: () => Decoder<a>): Decoder<a>
Decoder for recursive data structures.
A function that returns a decoder.
type Node<a> = {
value: a;
children?: Node<a>[];
};
const treeDecoder: JsonDecoder.Decoder<Node<string>> = JsonDecoder.object<
Node<string>
>(
{
value: JsonDecoder.string,
children: JsonDecoder.oneOf<Node<string>[]>(
[
JsonDecoder.lazy(() => JsonDecoder.array(treeDecoder, 'Node<a>[]')),
JsonDecoder.isUndefined([])
],
'Node<string>[] | isUndefined'
)
},
'Node<string>'
);
treeDecoder.decode({
value: 'root',
children: [
{ value: '1' },
{ value: '2', children: [{ value: '2.1' }, { value: '2.2' }] }
]
});
// Output: Ok<Node<string>>({value: {value: 'root', children: [....]}})
treeDecoder.decode({
value: 'root',
children: null
});
// Output: Err({error: "<Node<string>> decoder failed at key 'children' with error: <Node<string>[] | isUndefined> decoder failed because null can't be decoded with any of the provided oneOf decoders"})
optional<a>(decoder: Decoder<a>): Decoder<a | undefined>
The optional
decoder tries to decode the provided JSON with the provided decoder if the json value is not undefined
or null
. This decoder is to allow for an optional value in the TypeScript definition while retaining the ability to give a detailed error message if the wrapped decoder fails.
nullable<a>(decoder: Decoder<a>): Decoder<a | null>
The nullable
decoder tries to decode the provided JSON with the provided decoder, but allows for null
value. It returns a detailed error message if the value is not null
and the wrapped decoder fails.
interface User {
name: string;
email: string | null;
}
const userDecoder = JsonDecoder.object<User>(
{
name: JsonDecoder.string,
email: JsonDecoder.nullable(JsonDecoder.string)
},
'User'
);
userDecoder.decode({ name: 'Alice', email: 'alice@example.com' });
// Output: Ok<User>({value: {name: 'Alice', email: 'alice@example.com'}})
userDecoder.decode({ name: 'Alice', email: null });
// Output: Ok<User>({value: {name: 'Alice', email: null}})
userDecoder.decode({ name: 'Alice' });
// Output: Err({error: "<User> decoder failed at key 'email' with error: undefined is not a valid string"})
Decoder the JSON will be decoded with if the value is not null
or undefined
.
type User = {
firstname: string;
lastname: string;
email?: string;
};
const userDecoder = JsonDecoder.object<User>(
{
firstname: JsonDecoder.string,
lastname: JsonDecoder.string,
email: JsonDecoder.optional(JsonDecoder.string)
},
'User'
);
const jsonOk = {
firstname: 'Damien',
lastname: 'Jurado'
};
const jsonFullUser = {
firstname: 'Damien',
lastname: 'Jurado',
email: 'user@example.com'
};
const jsonKo = {
firstname: null,
lastname: 'Satie'
};
JsonDecoder.optional(userDecoder).decode(null);
// Output: Ok<User | undefined>({value: undefined})
JsonDecoder.optional(userDecoder).decode(undefined);
// Output: Ok<User | undefined>({value: undefined})
JsonDecoder.optional(userDecoder).decode(jsonOk);
// Output: Ok<User | undefined>({value: {firstname: 'Damien', lastname: 'Jurado', email: undefined}})
JsonDecoder.optional(userDecoder).decode(jsonFullUser);
// Output: Ok<User | undefined>({value: {firstname: 'Damien', lastname: 'Jurado', email: 'user@example.com'}})
JsonDecoder.optional(userDecoder).decode(jsonKo);
// Output: Err({error: '<User> decoder failed at key "firstname" with error: null is not a valid string'})
failover<a>(defaultValue: a, decoder: Decoder<a>): Decoder<a>
Creates a decoder that returns a default value on failure.
The Ok
default value when the decoder fails.
Decoder the JSON will be decoded with.
JsonDecoder.failover('default value', JsonDecoder.string).decode(
'This is fine'
);
// Ok<string>({value: 'This is fine'})
JsonDecoder.failover('default value', JsonDecoder.string).decode(null);
// Ok<string>({value: 'default value'})
succeed: Decoder<any>
Creates a decoder that always succeeds.
JsonDecoder.succeed.decode(null); // Ok<any>({value: null})
fail<a>(error: string): Decoder<a>
Creates a decoder that always fails.
Error message that will be returned with the Err
instance.
JsonDecoder.fail('Something wrong happened').decode('This is fine');
// Err({error: 'Something wrong happened'})
isNull<a>(defaultValue: a): Decoder<a>
Succeeds when JSON is strictly (===) null and returns a defaultValue.
Returned default value when JSON is null.
JsonDecoder.isNull('default value').decode(null);
// Ok({value: 'default value'})
JsonDecoder.isNull('default value').decode(999);
// Err({error: '999 is not null'})
isUndefined<a>(defaultValue: a): Decoder<a>
Succeeds when JSON is strictly (===) undefined and returns a defaultValue.
Returned default value when JSON is undefined.
JsonDecoder.isUndefined('default value').decode(undefined);
// Ok({value: 'default value'})
JsonDecoder.isUndefined('default value').decode(999);
// Err({error: '999 is not undefined'})
isExactly<a>(value: a): Decoder<a>
Succeeds when JSON is strictly (===) value: a
and returns value: a
.
Value returned when the JSON is strictly equal to it.
JsonDecoder.isExactly(true).decode(true);
// Ok({value: true})
JsonDecoder.isExactly(999).decode(true);
// Err({error: 'true is not 999'})
constant<a>(value: a): Decoder<a>
A Decoder that always succeeds, returning value
.
Value always returned.
JsonDecoder.constant(true).decode(false);
// Ok({value: true})
A combine
decoder tries to decode the provided JSON with all of the provided decoders and returns an intersection of them all.
Value always returned.
type User = { id: string };
type WithName = { name: string };
type WithAge = { age: number };
const userDecoder = JsonDecoder.object<User>(
{ id: JsonDecoder.string },
'User'
);
const nameDecoder = JsonDecoder.object<WithName>(
{ name: JsonDecoder.string },
'WithName'
);
const ageDecoder = JsonDecoder.object<WithAge>(
{ age: JsonDecoder.number },
'WithAge'
);
const finalDecoder = JsonDecoder.combine(userDecoder, nameDecoder, ageDecoder);
// Decoder<User & WithName & WithAge>
finalDecoder.decode({ id: 'alice', name: 'Alice', age: 30 });
// Ok({ id: 'alice', name: 'Alice', age: 30 })
finalDecoder.decode({ id: 'alice' });
// Err({ error: '<WithName> decoder failed at key "name" with error: undefined is not a valid string' })