ts.data.json helps you validate JSON data at runtime with compile-time type safety. This guide will show you how to use the library effectively.
Let's start with the basics. Here's how to decode simple JSON values:
import * as JsonDecoder from 'ts.data.json';
// String decoder
const nameDecoder = JsonDecoder.string();
nameDecoder.decode('John'); // Ok({ value: 'John' })
nameDecoder.decode(123); // Err({ issues: [{ message: '123 is not a valid string', path: [] }] })
// Number decoder
const ageDecoder = JsonDecoder.number();
ageDecoder.decode(25); // Ok({ value: 25 })
ageDecoder.decode('25'); // Err({ issues: [{ message: '"25" is not a valid number', path: [] }] })
// Boolean decoder
const isActiveDecoder = JsonDecoder.boolean();
isActiveDecoder.decode(true); // Ok({ value: true })
isActiveDecoder.decode('true'); // Err({ issues: [{ message: '"true" is not a valid boolean', path: [] }] })
Decode against a TypeScript enum with enumeration. A rejected value is
formatted with JSON.stringify, just like the other primitive decoders, so the
quoting depends on the value's type:
enum Color {
Red = 'red',
Blue = 'blue'
}
const colorDecoder = JsonDecoder.enumeration<Color>(Color);
colorDecoder.decode('red'); // Ok({ value: 'red' })
// A string value keeps its quotes...
colorDecoder.decode('green');
// Err({ issues: [{ message: '"green" is not a valid enum value', path: [] }] })
// ...while a numeric value is rendered without quotes.
enum Priority {
Low = 1,
High = 2
}
const priorityDecoder = JsonDecoder.enumeration<Priority>(Priority);
priorityDecoder.decode(3);
// Err({ issues: [{ message: '3 is not a valid enum value', path: [] }] })
Most of the time, you'll work with objects. Here's how to decode them:
// Define your type
interface User {
id: number;
name: string;
email: string;
age?: number; // Optional field
}
// Create a decoder
const userDecoder = JsonDecoder.object<User>({
id: JsonDecoder.number(),
name: JsonDecoder.string(),
email: JsonDecoder.string(),
age: JsonDecoder.optional(JsonDecoder.number())
});
// Valid data
const validJson = {
id: 1,
name: 'John Doe',
email: 'john@example.com',
age: 30
};
const user = await userDecoder.decodePromise(validJson);
console.log(`Hello ${user.name}!`); // Hello John Doe!
// Invalid data -- all field errors are reported at once
const invalidJson = {
id: 'not-a-number',
name: 'John Doe',
email: 'john@example.com'
};
try {
await userDecoder.decodePromise(invalidJson);
} catch (error) {
console.log(error.message); // 'id: "not-a-number" is not a valid number'
}
For complex objects with nested structures:
interface Address {
street: string;
city: string;
country: string;
}
interface User {
id: number;
name: string;
address: Address;
}
// Create decoders for nested structures
const addressDecoder = JsonDecoder.object<Address>({
street: JsonDecoder.string(),
city: JsonDecoder.string(),
country: JsonDecoder.string()
});
const userDecoder = JsonDecoder.object<User>({
id: JsonDecoder.number(),
name: JsonDecoder.string(),
address: addressDecoder // Use the nested decoder
});
const json = {
id: 1,
name: 'John Doe',
address: {
street: '123 Main St',
city: 'Boston',
country: 'USA'
}
};
console.log(
await userDecoder.decodePromise(json).then(user => `${user.name} lives in ${user.address.city}`) // John Doe lives in Boston
);
Decoding arrays of values:
// Array of strings
const tagsDecoder = JsonDecoder.array(JsonDecoder.string());
tagsDecoder.decode(['typescript', 'json', 'decoder']); // Ok({ value: ["typescript", "json", "decoder"] })
tagsDecoder.decode(['typescript', 123, 'decoder']);
// Err({ issues: [{ message: '123 is not a valid string', path: [1] }] })
// Array of objects
const usersDecoder = JsonDecoder.array(userDecoder);
await usersDecoder
.decodePromise([
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' }
])
.then(users => users.map(user => user.id)); // [1, 2]
usersDecoder.decode([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane', email: 'jane@example.com' }
]);
// Err({ issues: [{ message: 'undefined is not a valid string', path: [0, 'email'] }] })
Use fallback to provide fallback values:
const numberOrZero = JsonDecoder.fallback(0, JsonDecoder.number());
numberOrZero.decode('not a number'); // Ok({ value: 0 })
You can use other strategies combining other decoders:
const statusDecoder = JsonDecoder.oneOf([
JsonDecoder.literal('active'),
JsonDecoder.literal('inactive'),
JsonDecoder.constant('unknown') // always succeeds with 'unknown'
]);
statusDecoder.decode('inactive'); // Ok({ value: 'inactive' })
statusDecoder.decode('zxytwqgtyb'); // Ok({ value: 'unknown' })
The library uses a Result type to handle success and failure cases safely:
const myUserResult: JsonDecoder.Result<User> = userDecoder.decode(validUserJson);
const uppercasedUserEmail: JsonDecoder.Result<string> = myUserResult
.map(user => {
return user.email;
})
.map(email => {
return email.toUpperCase();
});
// isOk() is a type guard
if (uppercasedUserEmail.isOk()) {
console.log(uppercasedUserEmail.value); // JOHN@EXAMPLE.COM
}
When a decode fails, the Err result holds an issues array. Each entry contains a human-readable message and a path pointing to the failing field:
const result = userDecoder.decode({ id: 'bad', name: 42, email: 'john@example.com' });
if (!result.isOk()) {
result.issues.forEach(issue => {
const location = issue.path.length > 0 ? JsonDecoder.formatIssuePath(issue.path) : 'root';
console.log(`${location}: ${issue.message}`);
// id: "bad" is not a valid number
// name: 42 is not a valid string
});
}
You can use the FromDecoder type to infer types from decoders:
import { FromDecoder } from 'ts.data.json';
const userDecoder = JsonDecoder.object({
id: JsonDecoder.number(),
name: JsonDecoder.string(),
email: JsonDecoder.string()
});
// Instead of manually defining the User interface:
type User = JsonDecoder.FromDecoder<typeof userDecoder>;
// type User = { id: number; name: string; email: string }
Reuse Decoders: Create reusable decoders for common patterns:
const numToStringDecoder = JsonDecoder.number().map(n => n.toString(10));
numToStringDecoder.decode(123); // Ok({ value: "123" })
const dateDecoder = JsonDecoder.string().flatMap(...);
const emailDecoder = JsonDecoder.string().flatMap(...);
Type Safety: Let TypeScript help you by using type annotations and inference:
const myDecoder = JsonDecoder.object(...);
type User = JsonDecoder.FromDecoder<typeof myDecoder>;