ts.data.json - v4.1.0
    Preparing search index...

    Basic Usage

    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 }
    1. 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(...);
    2. Type Safety: Let TypeScript help you by using type annotations and inference:

       const myDecoder = JsonDecoder.object(...);
    type User = JsonDecoder.FromDecoder<typeof myDecoder>;