json-schema-to-ts

repository·main·Indexed 23 days ago

https://github.com/thomasaribart/json-schema-to-ts

A utility for inferring TypeScript types directly from JSON schemas, ensuring consistency between runtime validation and static type checking. It provides the FromSchema utility to derive types from primitives, arrays, tuples, and objects, and supports JSON Schema keywords such as anyOf, oneOf, allOf, and optional support for not and if/then/else. It is designed for developers who use JSON schemas (e.g., with AJV) and want to avoid duplicating definitions as TypeScript interfaces.

Tokens
12.9K
Snippets
27
Records
51
Agent score
79%

What's inside json-schema-to-ts

  1. What is json-schema-to-ts and when should I use it?

    main

    json-schema-to-ts is a tool for deriving TypeScript types from JSON schemas.

    Use it if:

    • You already use JSON schemas for runtime validation (e.g., with AJV) and want to avoid duplicating those definitions as TypeScript interfaces.
    • You want to benefit from the wide ecosystem and reusability of JSON schemas (Swagger, APIaaS, etc.).
    • You want zero impact on your compiled code (it operates entirely in the type space).

    Do NOT use it if:

    • You prefer schema-less validation libraries like zod, yup, or runtypes, which are often easier to use for pure TypeScript projects.
  2. Combine schemas using anyOf, oneOf, and allOf

    main

    The FromSchema utility can infer types from JSON Schema combination keywords:

    • anyOf: Inferred as a union of the possible schemas.
    • oneOf: Parsed similarly to anyOf, resulting in a union of types.
    • allOf: Inferred as an intersection (merging) of the properties defined in all schemas in the array.

    Note that for anyOf and oneOf, the resulting TypeScript type is a union. For allOf, the properties from all schemas are combined into a single object type.

    const anyOfSchema = {
      anyOf: [
        { type: "string" },
        {
          type: "array",
          items: { type: "string" },
        },
      ],
    } as const;
    
    type AnyOf = FromSchema<typeof anyOfSchema>;
    // => string | string[]
  3. Limitations of casting JSON schemas for type safety

    main

    When casting a JSON import to a narrow type to use with FromSchema, TypeScript only provides partial validation.

    • What is caught: Errors in object property names (keys) and structural mismatches that prevent the types from overlapping.
    • What is NOT caught: Errors in the values of the schema itself (e.g., using type: "number" instead of type: "string" or using const instead of enum) because the widening happens at the value level, not the key level.
    import { FromSchema } from "json-schema-to-ts";
    import dogRawSchema from "./dog.json";
    
    const dogSchema = dogoRawSchema as {
      type: "object";
      properties: {
        name: { type: "number" }; // UNDETECTED: "number" instead of "string" will go undetected
        years: { type: "integer" }; // DETECTED: "years" instead of "age" will throw an error
        hobbies: { type: "array"; items: { type: "string" } };
        favoriteFood: { const: "pizza" }; // DETECTED: "const" instead of "enum" will throw an error
      };
      required: ["name", "age"];
    };
  4. Using JSON file schemas with json-schema-to-ts

    main

    By default, json-schema-to-ts does not work directly with imported .json files because TypeScript treats JSON imports as widened types (e.g., { type: string } instead of the narrow { type: "string" }). FromSchema requires narrow types to perform correct type computations.

    To use a .json file, you must manually cast the imported schema to a narrow type using as. While this involves some code duplication, TypeScript provides partial type safety: it will throw an error if the narrow type you provide does not sufficiently overlap with the actual structure of the JSON file (for example, if property names differ).

    import { FromSchema } from "json-schema-to-ts";
    import dogRawSchema from "./dog.json";
    
    // Cast the raw JSON import to a narrow type to satisfy FromSchema
    const dogSchema = dogRawSchema as {
      type: "object";
      properties: {
        name: { type: "string" };
        age: { type: "integer" };
        hobbies: { type: "array"; items: { type: "string" } };
        favoriteFood: { enum: ["pizza", "taco", "fries"] };
      };
      required: ["name", "age"];
    };
    
    type Dog = FromSchema<typeof dogSchema>;
  5. Install json-schema-to-ts

    main

    Install json-schema-to-ts as a development dependency using npm or yarn.

    Requirements:

    • TypeScript 4.3+
    • TypeScript strict mode must be enabled.
    • Ensure noStrictGenericChecks is turned off in your tsconfig.json.
    # npm
    npm install --save-dev json-schema-to-ts
    
    # yarn
    yarn add --dev json-schema-to-ts
  6. What is ExtendedJSONSchema and how do I use it?

    main

    In json-schema-to-ts, ExtendedJSONSchema<EXTENSION> is a powerful type that combines the standard JSON Schema structure with a user-defined EXTENSION.

    How it works

    • Standard Fields: It includes all standard JSON Schema keywords like type, properties, items, allOf, anyOf, etc.
    • Custom Fields: It uses the EXTENSION generic to allow any additional properties defined in your custom interface.
    • Recursive Support: The extension is applied recursively to nested schemas within properties, items, definitions, and other structural keywords.
    • OpenAPI Support: It includes nullable?: boolean as a built-in extension common in OpenAPI specs.

    When to use it

    Use ExtendedJSONSchema instead of a plain JSON Schema type whenever you are working with schemas that include vendor-specific extensions (like x- properties) or specialized metadata that you want to keep typed within your TypeScript codebase.

  7. Understand the difference between $Compiler and Compiler

    main

    The library distinguishes between two types of compiler functions:

    1. $Compiler: A standard function that takes a JSONSchema and returns a validator function. The validator function returns a simple boolean indicating if the data is valid.

    2. Compiler: An enhanced version of the compiler that returns a validator function acting as a type guard. When used, the validator function uses the data is T syntax, where T is the TypeScript type automatically inferred from the schema using FromSchema.

  8. Apply FromSchema to generics

    main

    When using FromSchema within a generic function (for example, a library function that accepts a schema and returns data inferred from it), TypeScript may throw a type instantiation is excessively deep and possibly infinite error.

    To resolve this, you must introduce a second generic parameter to your type definition that explicitly captures the result of FromSchema<SCHEMA> as a default value. This prevents the recursive type instantiation error during the inference process.

    import { FromSchema, JSONSchema } from "json-schema-to-ts";
    
    // Use a second generic 'DATA' with a default value of 'FromSchema<SCHEMA>' 
    // to avoid 'type instantiation is excessively deep' errors.
    type Mocker = <SCHEMA extends JSONSchema, DATA = FromSchema<SCHEMA>>(
      schema: SCHEMA,
    ) => DATA;
    
    const getMockedData: Mocker = schema => {
      // ... logic here
    };
    
    const dogSchema = {
      type: "object",
      // ... schema here
    } as const;
    
    // This will now work correctly
    const dogMock = getMockedData(dogSchema);
  9. Fix 'type instantiation is excessively deep and potentially infinite' error

    main

    This error occurs when the TypeScript compiler detects long type computations or potential infinite loops during the complex recursive type processing used by FromSchema. This is most common when processing very large schemas or schemas that heavily use intersections (allOf) and exclusions (not, else, ifThenElse).

    If you encounter this error, you can try the following steps:

    1. Opt-out of exclusions: Try removing or simplifying keywords like not or ifThenElse to reduce recursion depth.
    2. Use @ts-ignore: If the inferred type is still valid (i.e., it does not result in an any type), you can suppress the error using a // @ts-ignore comment.
    3. Open an issue: If the error persists and prevents valid type usage, report the issue to the maintainer.
  10. Convert JSON Schema constants and enums to TypeScript types

    main

    Use FromSchema to extract literal types from const schemas or to convert JSON Schema enum arrays into TypeScript union types. You can also pass TypeScript enum values into an enum schema to map them back to the TypeScript enum type.

    // Const
    const fooSchema = {
      const: "foo",
    } as const;
    
    type Foo = FromSchema<typeof fooSchema>;
    // => "foo"
    
    // Enums
    const enumSchema = {
      enum: [true, 42, { foo: "bar" }],
    } as const;
    
    type Enum = FromSchema<typeof enumSchema>;
    // => true | 42 | { foo: "bar"}
    
    // Using TypeScript enums
    enum Food {
      Pizza = "pizza",
      Taco = "taco",
      Fries = "fries",
    }
    
    const enumSchema = {
      enum: Object.values(Food),
    } as const;
    
    type Enum = FromSchema<typeof enumSchema>;
    // => Food
  11. Convert primitive types and nullable schemas

    main

    Map JSON Schema primitive types (null, boolean, string, integer, number) to TypeScript types. Use the nullable: true keyword to create union types with null.

    // Single primitive
    const primitiveTypeSchema = {
      type: "null", // "boolean", "string", "integer", "number"
    } as const;
    
    type PrimitiveType = FromSchema<typeof primitiveTypeSchema>;
    // => null, boolean, string or number
    
    // Union of primitives
    const primitiveTypesSchema = {
      type: ["null", "string"],
    } as const;
    
    type PrimitiveTypes = FromSchema<typeof primitiveTypesSchema>;
    // => null | string
    
    // Nullable
    const nullableSchema = {
      type: "string",
      nullable: true,
    } as const;
    
    type Nullable = FromSchema<typeof nullableSchema>;
    // => string | null
  12. Convert Objects and manage property optionality

    main

    Convert JSON Schema objects to TypeScript interfaces/types.

    Defaulted Properties

    By default, properties with a default value are treated as required in the resulting TypeScript type. To keep them optional, pass { keepDefaultedPropertiesOptional: true } as the second argument to FromSchema.

    Controlling Additional Properties

    • Deny extra properties: Use additionalProperties: false or unevaluatedProperties: false (when used with allOf).
    • Type unnamed properties: Use additionalProperties or patternProperties to define types for keys not explicitly listed in properties.
    • Conflict Resolution: If properties is used alongside additionalProperties or patternProperties, extra properties are typed as unknown to prevent type conflicts.
    • Limitation: unevaluatedProperties does not type extra properties when used on its own; use additionalProperties for that purpose.
    // Basic Object
    const objectSchema = {
      type: "object",
      properties: {
        foo: { type: "string" },
        bar: { type: "number" },
      },
      required: ["foo"],
    } as const;
    
    type Object = FromSchema<typeof objectSchema>;
    // => { [x: string]: unknown; foo: string; bar?: number; }
    
    // Handling Defaulted Properties
    const defaultedProp = {
      type: "object",
      properties: {
        foo: { type: "string", default: "bar" },
      },
      additionalProperties: false,
    } as const;
    
    // Default behavior: foo is required
    type Object = FromSchema<typeof defaultedProp>;
    // => { foo: string; }
    
    // Custom behavior: foo is optional
    type Object = FromSchema<
      typeof defaultedProp,
      { keepDefaultedPropertiesOptional: true }
    >;
    // => { foo?: string; }
    
    // Denying additional properties
    const closedObjectSchema = {
      ...objectSchema,
      additionalProperties: false,
    } as const;
    
    type Object = FromSchema<typeof closedObjectSchema>;
    // => { foo: string; bar?: number; }
    
    // Using unevaluatedProperties with allOf
    const closedObjectSchema = {
      type: "object",
      allOf: [
        {
          properties: {
            foo: { type: "string" },
          },
          required: ["foo"],
        },
        {
          properties: {
            bar: { type: "number" },
          },
        },
      ],
      unevaluatedProperties: false,
    } as const;
    
    type Object = FromSchema<typeof closedObjectSchema>;
    // => { foo: string; bar?: number; }
    
    // Typing unnamed properties via patternProperties/additionalProperties
    const openObjectSchema = {
      type: "object",
      additionalProperties: {
        type: "boolean",
      },
      patternProperties: {
        "^S": { type: "string" },
        "^I": { type: "integer" },
      },
    } as const;
    
    type Object = FromSchema<typeof openObjectSchema>;
    // => { [x: string]: string | number | boolean }
    
    // Conflict: properties + additionalProperties results in unknown for extras
    const mixedObjectSchema = {
      type: "object",
      properties: {
        foo: { enum: ["bar", "baz"] },
      },
      additionalProperties: { type: "string" },
    } as const;
    
    type Object = FromSchema<typeof mixedObjectSchema>;
    // => { [x: string]: unknown; foo?: "bar" | "baz"; }