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

    Advanced Usage

    This guide covers advanced patterns and features of ts.data.json. For basic usage, see the Basic Usage guide.

    You can easily replicate the string decoder:

    import * as JsonDecoder from 'ts.data.json';
    import { ok, err } from 'ts.data.json';

    const myStringDecoder: JsonDecoder.Decoder<string> = new JsonDecoder.Decoder((json: unknown) => {
    if (typeof json === 'string') {
    return ok(json);
    } else {
    return err([{ message: 'Expected a string', path: [] }]);
    }
    });

    console.log(myStringDecoder.decode('Hello!')); // Ok({ value: 'Hello!' })
    console.log(myStringDecoder.decode(123)); // Err({ issues: [{ message: 'Expected a string', path: [] }] })

    Leverage built-in decoders and layer other decoders on top by following this pattern with the flatMap function.

    const emailDecoder = JsonDecoder.string().flatMap(email => {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email) ? JsonDecoder.succeed() : JsonDecoder.fail(`Invalid email format: ${email}`);
    });

    emailDecoder.decode('user@example.com'); // Ok({ value: 'user@example.com' })
    emailDecoder.decode('not-an-email'); // Err({ issues: [{ message: 'Invalid email format: not-an-email', path: [] }] })
    const dateDecoder = JsonDecoder.string().flatMap(str => {
    const date = new Date(str);
    return isNaN(date.getTime()) ? JsonDecoder.fail(`Invalid date format: ${str}`) : JsonDecoder.succeed();
    });

    dateDecoder.decode('2024-01-15'); // Ok({ value: '2024-01-15' })
    dateDecoder.decode('not-a-date'); // Err({ issues: [{ message: 'Invalid date format: not-a-date', path: [] }] })
    const ageDecoder = JsonDecoder.number().flatMap(age => {
    return age >= 0 && age <= 120 ? JsonDecoder.succeed() : JsonDecoder.fail(`Age must be between 0 and 120, got: ${age}`);
    });

    ageDecoder.decode(25); // Ok({ value: 25 })
    ageDecoder.decode(200); // Err({ issues: [{ message: 'Age must be between 0 and 120, got: 200', path: [] }] })

    Handle recursive data structures like trees or linked lists:

    interface TreeNode {
    value: string;
    children?: TreeNode[];
    }

    const treeDecoder: JsonDecoder.Decoder<TreeNode> = JsonDecoder.lazy(() =>
    JsonDecoder.object<TreeNode>({
    value: JsonDecoder.string(),
    children: JsonDecoder.optional(JsonDecoder.array(treeDecoder))
    })
    );

    const tree = {
    value: 'root',
    children: [
    { value: 'child1' },
    {
    value: 'child2',
    children: [{ value: 'grandchild' }]
    }
    ]
    };

    treeDecoder.decode(tree).map(node => console.log(JSON.stringify(node, null, 2))); // Ok(...)

    const badTree = { ...tree, children: [...tree.children, { value: 12 }] };
    treeDecoder.decode(badTree);
    // Err({ issues: [{ message: '12 is not a valid string', path: ['children', 2, 'value'] }] })

    For composite types, pick the combinator that matches the shape:

    You want… Use
    One of several alternatives, tried in order oneOf([a, b, …])
    A tagged union of objects (shared literal field) discriminatedUnion('kind', { … })
    To merge several object decoders (intersection / A & B) allOf([a, b, …])
    A value that may be null nullable(decoder)
    A value that may be undefined optional(decoder)

    oneOf is the general union: it returns the first match and, on failure, reports every alternative. Prefer nullable/optional over oneOf([x, null])/oneOf([x, undefined]).

    When your variants are objects sharing a literal "tag" field, reach for discriminatedUnion. Knowing the tag field, it validates only the matching variant and produces precise, single-variant errors — rather than oneOf, which would try every branch and report all of their failures.

    type Shape = { type: 'circle'; radius: number } | { type: 'rectangle'; width: number; height: number };

    const shapeDecoder = JsonDecoder.discriminatedUnion('type', {
    circle: JsonDecoder.object<Extract<Shape, { type: 'circle' }>>({
    type: JsonDecoder.literal('circle'),
    radius: JsonDecoder.number()
    }),
    rectangle: JsonDecoder.object<Extract<Shape, { type: 'rectangle' }>>({
    type: JsonDecoder.literal('rectangle'),
    width: JsonDecoder.number(),
    height: JsonDecoder.number()
    })
    });

    // Usage
    const shapes = [
    { type: 'circle', radius: 5 },
    { type: 'rectangle', width: 10, height: 20 }
    ];

    console.log(
    JsonDecoder.array(shapeDecoder)
    .decode(shapes)
    .map(decodedShapes =>
    decodedShapes.map(shape => {
    if (shape.type === 'circle') {
    return `Circle area: ${Math.PI * shape.radius ** 2}`;
    } else {
    return `Rectangle area: ${shape.width * shape.height}`;
    }
    })
    )
    ); // Ok({ value: ["Circle area: 78.53981633974483", "Rectangle area: 200"] })

    // A wrong field reports only the matching variant's failure:
    shapeDecoder.decode({ type: 'circle', radius: 'big' });
    // Err -> radius: "big" is not a valid number

    // An unknown tag lists the expected values:
    shapeDecoder.decode({ type: 'triangle' });
    // Err -> type: "type" must be one of "circle", "rectangle", but got "triangle"

    Transform decoded data into different structures:

    type SnakeToCamel<S extends string> = S extends `${infer T}_${infer U}${infer Rest}` ? `${T}${Uppercase<U>}${SnakeToCamel<Rest>}` : S;
    type CamelizedRecord<T extends Record<string, unknown>> = {
    [K in keyof T as SnakeToCamel<K & string>]: T[K];
    };

    function camelizeRecord<T extends Record<string, unknown>>(decoder: JsonDecoder.Decoder<T>): JsonDecoder.Decoder<CamelizedRecord<T>> {
    function snakeToCamel(str: string): string {
    return str
    .toLowerCase() // Ensure lowercase input
    .replace(/[_]+([a-z])/g, (_, letter) => letter.toUpperCase()) // Convert _x to X
    .replace(/^_+|_+$/g, ''); // Remove leading/trailing underscores
    }
    return decoder.flatMap(record => {
    const camelizedRecord = Object.keys(record).reduce((acc, key) => {
    const k = snakeToCamel(key);
    (acc as Record<string, unknown>)[k] = record[key];
    return acc;
    }, {} as CamelizedRecord<T>);
    return JsonDecoder.constant(camelizedRecord);
    });
    }

    const camelizeApiUserDecoder = camelizeRecord(
    JsonDecoder.object({
    id: JsonDecoder.number(),
    first_name: JsonDecoder.string(),
    last_name: JsonDecoder.string(),
    email_address: JsonDecoder.string()
    })
    );

    type User = JsonDecoder.FromDecoder<typeof camelizeApiUserDecoder>;

    const apiUserJson = {
    id: 1,
    first_name: 'John', // Notice these are snake cased!
    last_name: 'Doe',
    email_address: 'john@doe.com'
    };

    const user: User = await camelizeApiUserDecoder.decodePromise(apiUserJson);
    // { id: 1, firstName: 'John', lastName: 'Doe', emailAddress: 'john@doe.com' }

    Ensure no extra properties exist in objects:

    interface MiniUser {
    id: number;
    name: string;
    }

    const strictUserDecoder = JsonDecoder.objectStrict<MiniUser>({
    id: JsonDecoder.number(),
    name: JsonDecoder.string()
    });

    // This will fail because of extra properties
    strictUserDecoder.decode({
    id: 1,
    name: 'John',
    extra: 'field'
    });
    // Err({ issues: [{ message: 'Unknown key "extra" found in strict object', path: [] }] })

    Handle objects with dynamic keys:

    interface MiniUser {
    id: number;
    name: string;
    }

    const miniUserDecoder = JsonDecoder.object<MiniUser>({
    id: JsonDecoder.number(),
    name: JsonDecoder.string()
    });

    // Map of user IDs to users
    const userMapDecoder = JsonDecoder.record(miniUserDecoder);

    const users = {
    user1: { id: 1, name: 'John' },
    user2: { id: 2, name: 'Jane' }
    };

    userMapDecoder.decode(users).map(userMap => {
    console.log(userMap['user1']); // { id: 1, name: "John" }
    });
    1. Modular Decoders: Break down complex decoders into smaller, reusable parts:

      const baseUserDecoder = JsonDecoder.object({...});
      const adminUserDecoder = baseUserDecoder.flatMap(user => ...);
      const regularUserDecoder = baseUserDecoder.flatMap(user => ...);
    2. Validation Factories: Create functions that generate common validation patterns:

      const createRangeDecoder = (min: number, max: number, name: string) => JsonDecoder.number().flatMap(n => (n >= min && n <= max ? JsonDecoder.succeed() : JsonDecoder.fail(`${name} must be between ${min} and ${max}`)));

      const ageDecoder = createRangeDecoder(0, 120, 'Age');
      const percentageDecoder = createRangeDecoder(0, 100, 'Percentage');
    3. Error Context: Add meaningful context to error messages:

    const dateDecoder = JsonDecoder.string().flatMap(str => {
    const date = new Date(str);
    return isNaN(date.getTime()) ? JsonDecoder.fail(`Invalid date format: ${str}`) : JsonDecoder.succeed();
    });