json-schema-to-ts
Infer TS types from JSON schemas
README
If you use this repo, star it ✨
Stop typing twice 🙅♂️
- ```typescript
- const dogSchema = {
- 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 = {
- name: string;
- age: number;
- hobbies?: string[];
- favoriteFood?: "pizza" | "taco" | "fries";
- };
- ```
FromSchema
- ```typescript
- import { FromSchema } from "json-schema-to-ts";
- const dogSchema = {
- type: "object",
- properties: {
- name: { type: "string" },
- age: { type: "integer" },
- hobbies: { type: "array", items: { type: "string" } },
- favoriteFood: { enum: ["pizza", "taco", "fries"] },
- },
- required: ["name", "age"],
- } as const;
- type Dog = FromSchema<typeof dogSchema>;
- // => Will infer the same type as above
- ```
- ```typescript
- const catSchema = { ... } as const;
- const petSchema = {
- anyOf: [dogSchema, catSchema],
- } as const;
- type Pet = FromSchema<typeof petSchema>;
- // => Will work 🙌
- ```
- ```typescript
- import { asConst } from "json-schema-to-ts";
- const dogSchema = asConst({
- type: "object",
- ...
- });
- type Dog = FromSchema<typeof dogSchema>;
- // => Will work as well 🙌
- ```
Why use json-schema-to-ts?
- ```typescript
- const addressSchema = {
- type: "object",
- allOf: [
- {
- properties: {
- street: { type: "string" },
- city: { type: "string" },
- state: { type: "string" },
- },
- required: ["street", "city", "state"],
- },
- {
- properties: {
- type: { enum: ["residential", "business"] },
- },
- },
- ],
- additionalProperties: false,
- } as const;
- ```
- ```typescript
- type Address = FromSchema<typeof addressSchema>;
- // => never 🙌
- ```
\*If json-schema-to-ts misses one of your use case, feel free to open an issue 🤗
Table of content
Installation
- ``` sh
- # npm
- npm install --save-dev json-schema-to-ts
- # yarn
- yarn add --dev json-schema-to-ts
- ```
json-schema-to-ts requires TypeScript 4.3+. Using strict mode is required, as well as (apparently) turning off [noStrictGenericChecks](https://www.typescriptlang.org/tsconfig#noStrictGenericChecks).
Use cases
Const
- ```typescript
- const fooSchema = {
- const: "foo",
- } as const;
- type Foo = FromSchema<typeof fooSchema>;
- // => "foo"
- ```
Enums
- ```typescript
- const enumSchema = {
- enum: [true, 42, { foo: "bar" }],
- } as const;
- type Enum = FromSchema<typeof enumSchema>;
- // => true | 42 | { foo: "bar"}
- ```
- ```typescript
- enum Food {
- Pizza = "pizza",
- Taco = "taco",
- Fries = "fries",
- }
- const enumSchema = {
- enum: Object.values(Food),
- } as const;
- type Enum = FromSchema<typeof enumSchema>;
- // => Food
- ```
Primitive types
- ```typescript
- const primitiveTypeSchema = {
- type: "null", // "boolean", "string", "integer", "number"
- } as const;
- type PrimitiveType = FromSchema<typeof primitiveTypeSchema>;
- // => null, boolean, string or number
- ```
- ```typescript
- const primitiveTypesSchema = {
- type: ["null", "string"],
- } as const;
- type PrimitiveTypes = FromSchema<typeof primitiveTypesSchema>;
- // => null | string
- ```
For more complex types, refinment keywords like required or additionalItems will apply 🙌
Nullable
- ```typescript
- const nullableSchema = {
- type: "string",
- nullable: true,
- } as const;
- type Nullable = FromSchema<typeof nullableSchema>;
- // => string | null
- ```
Arrays
- ```typescript
- const arraySchema = {
- type: "array",
- items: { type: "string" },
- } as const;
- type Array = FromSchema<typeof arraySchema>;
- // => string[]
- ```
Tuples
- ```typescript
- const tupleSchema = {
- type: "array",
- items: [{ type: "boolean" }, { type: "string" }],
- } as const;
- type Tuple = FromSchema<typeof tupleSchema>;
- // => [] | [boolean] | [boolean, string] | [boolean, string, ...unknown[]]
- ```
- ```typescript
- const tupleSchema = {
- type: "array",
- items: [{ type: "boolean" }, { type: "string" }],
- additionalItems: false,
- } as const;
- type Tuple = FromSchema<typeof tupleSchema>;
- // => [] | [boolean] | [boolean, string]
- ```
- ```typescript
- const tupleSchema = {
- type: "array",
- items: [{ type: "boolean" }, { type: "string" }],
- additionalItems: { type: "number" },
- } as const;
- type Tuple = FromSchema<typeof tupleSchema>;
- // => [] | [boolean] | [boolean, string] | [boolean, string, ...number[]]
- ```
- ```typescript
- const tupleSchema = {
- type: "array",
- items: [{ type: "boolean" }, { type: "string" }],
- minItems: 1,
- maxItems: 2,
- } as const;
- type Tuple = FromSchema<typeof tupleSchema>;
- // => [boolean] | [boolean, string]
- ```
Additional items will only work if Typescript's strictNullChecks option is activated
Objects
- ```typescript
- 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; }
- ```
- ```typescript
- const closedObjectSchema = {
- ...objectSchema,
- additionalProperties: false,
- } as const;
- type Object = FromSchema<typeof closedObjectSchema>;
- // => { foo: string; bar?: number; }
- ```
- ```typescript
- 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 }
- ```
Combining schemas
AnyOf
- ```typescript
- const anyOfSchema = {
- anyOf: [
- { type: "string" },
- {
- type: "array",
- items: { type: "string" },
- },
- ],
- } as const;
- type AnyOf = FromSchema<typeof anyOfSchema>;
- // => string | string[]
- ```
- ```typescript
- const factoredSchema = {
- type: "object",
- properties: {
- bool: { type: "boolean" },
- },
- required: ["bool"],
- anyOf: [
- {
- properties: {
- str: { type: "string" },
- },
- required: ["str"],
- },
- {
- properties: {
- num: { type: "number" },
- },
- },
- ],
- } as const;
- type Factored = FromSchema<typeof factoredSchema>;
- // => {
- // [x:string]: unknown;
- // bool: boolean;
- // str: string;
- // } | {
- // [x:string]: unknown;
- // bool: boolean;
- // num?: number;
- // }
- ```
OneOf
- ```typescript
- const catSchema = {
- type: "object",
- oneOf: [
- {
- properties: {
- name: { type: "string" },
- },
- required: ["name"],
- },
- {
- properties: {
- color: { enum: ["black", "brown", "white"] },
- },
- },
- ],
- } as const;
- type Cat = FromSchema<typeof catSchema>;
- // => {
- // [x: string]: unknown;
- // name: string;
- // } | {
- // [x: string]: unknown;
- // color?: "black" | "brown" | "white";
- // }
- // => Error will NOT be raised 😱
- const invalidCat: Cat = { name: "Garfield" };
- ```
AllOf
- ```typescript
- const addressSchema = {
- type: "object",
- allOf: [
- {
- properties: {
- address: { type: "string" },
- city: { type: "string" },
- state: { type: "string" },
- },
- required: ["address", "city", "state"],
- },
- {
- properties: {
- type: { enum: ["residential", "business"] },
- },
- },
- ],
- } as const;
- type Address = FromSchema<typeof addressSchema>;
- // => {
- // [x: string]: unknown;
- // address: string;
- // city: string;
- // state: string;
- // type?: "residential" | "business";
- // }
- ```
Not
- ```typescript
- const tupleSchema = {
- type: "array",
- items: [{ const: 1 }, { const: 2 }],
- additionalItems: false,
- not: {
- const: [1],
- },
- } as const;
- type Tuple = FromSchema<typeof tupleSchema, { parseNotKeyword: true }>;
- // => [] | [1, 2]
- ```
- ```typescript
- const primitiveTypeSchema = {
- not: {
- type: ["array", "object"],
- },
- } as const;
- type PrimitiveType = FromSchema<
- typeof primitiveTypeSchema,
- { parseNotKeyword: true }
- >;
- // => null | boolean | number | string
- ```
- ```typescript
- // 👍 Can be propagated on "animal" property
- const petSchema = {
- type: "object",
- properties: {
- animal: { enum: ["cat", "dog", "boat"] },
- },
- not: {
- properties: { animal: { const: "boat" } },
- },
- required: ["animal"],
- additionalProperties: false,
- } as const;
- type Pet = FromSchema<typeof petSchema, { parseNotKeyword: true }>;
- // => { animal: "cat" | "dog" }
- ```
- ```typescript
- // ❌ Cannot be propagated
- const petSchema = {
- type: "object",
- properties: {
- animal: { enum: ["cat", "dog"] },
- color: { enum: ["black", "brown", "white"] },
- },
- not: {
- const: { animal: "cat", color: "white" },
- },
- required: ["animal", "color"],
- additionalProperties: false,
- } as const;
- type Pet = FromSchema<typeof petSchema, { parseNotKeyword: true }>;
- // => { animal: "cat" | "dog", color: "black" | "brown" | "white" }
- ```
- ```typescript
- const oddNumberSchema = {
- type: "number",
- not: { multipleOf: 2 },
- } as const;
- type OddNumber = FromSchema<typeof oddNumberSchema, { parseNotKeyword: true }>;
- // => should and will resolve to "number"
- const incorrectSchema = {
- type: "number",
- not: { bogus: "option" },
- } as const;
- type Incorrect = FromSchema<typeof incorrectSchema, { parseNotKeyword: true }>;
- // => should resolve to "never" but will still resolve to "number"
- ```
- ```typescript
- const goodLanguageSchema = {
- type: "string",
- not: {
- enum: ["Bummer", "Silly", "Lazy sod !"],
- },
- } as const;
- type GoodLanguage = FromSchema<
- typeof goodLanguageSchema,
- { parseNotKeyword: true }
- >;
- // => string
- ```
If/Then/Else
- ```typescript
- const petSchema = {
- type: "object",
- properties: {
- animal: { enum: ["cat", "dog"] },
- dogBreed: { enum: Object.values(DogBreed) },
- catBreed: { enum: Object.values(CatBreed) },
- },
- required: ["animal"],
- if: {
- properties: {
- animal: { const: "dog" },
- },
- },
- then: {
- required: ["dogBreed"],
- not: { required: ["catBreed"] },
- },
- else: {
- required: ["catBreed"],
- not: { required: ["dogBreed"] },
- },
- additionalProperties: false,
- } as const;
- type Pet = FromSchema<typeof petSchema, { parseIfThenElseKeywords: true }>;
- // => {
- // animal: "dog";
- // dogBreed: DogBreed;
- // catBreed?: CatBreed | undefined
- // } | {
- // animal: "cat";
- // catBreed: CatBreed;
- // dogBreed?: DogBreed | undefined
- // }
- ```
☝️ FromSchema computes the resulting type as (If ∩ Then) ∪ (¬If ∩ Else). While correct in theory, remember that the not keyword is not perfectly assimilated, which may become an issue in some complex schemas.
Definitions
- ```typescript
- const userSchema = {
- type: "object",
- properties: {
- name: { $ref: "#/$defs/name" },
- age: { $ref: "#/$defs/age" },
- },
- required: ["name", "age"],
- additionalProperties: false,
- $defs: {
- name: { type: "string" },
- age: { type: "integer" },
- },
- } as const;
- type User = FromSchema<typeof userSchema>;
- // => {
- // name: string;
- // age: number;
- // }
- ```
☝️ Wether in definitions or references, FromSchema will not work on recursive schemas for now.
References
- ```typescript
- const userSchema = {
- $id: "http://example.com/schemas/user.json",
- type: "object",
- properties: {
- name: { type: "string" },
- age: { type: "integer" },
- },
- required: ["name", "age"],
- additionalProperties: false,
- } as const;
- const usersSchema = {
- type: "array",
- items: {
- $ref: "http://example.com/schemas/user.json",
- },
- } as const;
- type Users = FromSchema<
- typeof usersSchema,
- { references: [typeof userSchema] }
- >;
- // => {
- // name: string;
- // age: string;
- // }[]
- const anotherUsersSchema = {
- $id: "http://example.com/schemas/users.json",
- type: "array",
- items: { $ref: "user.json" },
- } as const;
- // => Will work as well 🙌
- ```
Deserialization
- ```typescript
- const userSchema = {
- type: "object",
- properties: {
- name: { type: "string" },
- email: {
- type: "string",
- format: "email",
- },
- birthDate: {
- type: "string",
- format: "date-time",
- },
- },
- required: ["name", "email", "birthDate"],
- additionalProperties: false,
- } as const;
- type Email = string & { brand: "email" };
- type User = FromSchema<
- typeof userSchema,
- {
- deserialize: [
- {
- pattern: {
- type: "string";
- format: "email";
- };
- output: Email;
- },
- {
- pattern: {
- type: "string";
- format: "date-time";
- };
- output: Date;
- }
- ];
- }
- >;
- // => {
- // name: string;
- // email: Email;
- // birthDate: Date;
- // }
- ```
Typeguards
- ```typescript
- import { FromSchema, Validator } from "json-schema-to-ts";
- // It's important to:
- // - Explicitely type your validator as Validator
- // - Use FromSchema as the default value of a 2nd generic first
- const validate: Validator = <S extends JSONSchema, T = FromSchema<S>>(
- schema: S,
- data: unknown
- ): data is T => {
- const isDataValid: boolean = ... // Implement validation here
- return isDataValid;
- };
- const petSchema = { ... } as const
- let data: unknown;
- if (validate(petSchema, data)) {
- const { name, ... } = data; // data is typed as Pet 🙌
- }
- ```
- ```typescript
- type FromSchemaOptions = { parseNotKeyword: true };
- type ValidationOptions = [{ fastValidate: boolean }]
- const validate: Validator<FromSchemaOptions, ValidationOptions> = <
- S extends JSONSchema,
- T = FromSchema<S, FromSchemaOptions>
- >(
- schema: S,
- data: unknown,
- ...validationOptions: ValidationOptions
- ): data is T => { ... };
- ```
Validators
- ```typescript
- import Ajv from "ajv";
- import { $Validator, wrapValidatorAsTypeGuard } from "json-schema-to-ts";
- const ajv = new Ajv();
- // The initial validator definition is up to you
- // ($Validator is prefixed with $ to differ from resulting type guard)
- const $validate: $Validator = (schema, data) => ajv.validate(schema, data);
- const validate = wrapValidatorAsTypeGuard($validate);
- const petSchema = { ... } as const;
- let data: unknown;
- if (validate(petSchema, data)) {
- const { name, ... } = data; // data is typed as Pet 🙌
- }
- ```
- ```typescript
- type FromSchemaOptions = { parseNotKeyword: true };
- type ValidationOptions = [{ fastValidate: boolean }]
- const $validate: $Validator<ValidationOptions> = (
- schema,
- data,
- ...validationOptions // typed as ValidationOptions
- ) => { ... };
- // validate will inherit from ValidationOptions
- const validate = wrapValidatorAsTypeGuard($validate);
- // with special FromSchemaOptions
- // (ValidationOptions needs to be re-provided)
- const validate = wrapValidatorAsTypeGuard<
- FromSchemaOptions,
- ValidationOptions
- >($validate);
- ```
Compilers
- ```typescript
- import Ajv from "ajv";
- import { $Compiler, wrapCompilerAsTypeGuard } from "json-schema-to-ts";
- // The initial compiler definition is up to you
- // ($Compiler is prefixed with $ to differ from resulting type guard)
- const $compile: $Compiler = (schema) => ajv.compile(schema);
- const compile = wrapCompilerAsTypeGuard($compile);
- const petSchema = { ... } as const;
- const isPet = compile(petSchema);
- let data: unknown;
- if (isPet(data)) {
- const { name, ... } = data; // data is typed as Pet 🙌
- }
- ```
- ```typescript
- type FromSchemaOptions = { parseNotKeyword: true };
- type CompilingOptions = [{ fastCompile: boolean }];
- type ValidationOptions = [{ fastValidate: boolean }];
- const $compile: $Compiler<CompilingOptions, ValidationOptions> = (
- schema,
- ...compilingOptions // typed as CompilingOptions
- ) => {
- ...
- return (
- data,
- ...validationOptions // typed as ValidationOptions
- ) => { ... };
- };
- // compile will inherit from all options
- const compile = wrapCompilerAsTypeGuard($compile);
- // with special FromSchemaOptions
- // (options need to be re-provided)
- const compile = wrapCompilerAsTypeGuard<
- FromSchemaOptions,
- CompilingOptions,
- ValidationOptions
- >($compile);
- ```