Stop typing twice 🙅♂️
A lot of projects use JSON schemas for runtime data validation along with TypeScript for static type checking.
Their code may look like this:
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";
};
Both objects carry similar if not exactly the same information. This is a code duplication that can annoy developers and introduce bugs if not properly maintained.
That's when json-schema-to-ts
comes to the rescue 💪
FromSchema
The FromSchema
method lets you infer TS types directly from JSON schemas:
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>;
Schemas can even be nested, as long as you don't forget the as const
statement:
const catSchema = {
type: "object",
properties: {
name: { type: "string" },
age: { type: "integer" },
favoriteThings: { enum: ["playing", "sleeping", "moreSleeping"] },
},
required: ["name", "age"],
} as const;
const petSchema = {
anyOf: [dogSchema, catSchema],
} as const;
type Pet = FromSchema<typeof petSchema>;
The as const
statement is used so that TypeScript takes the schema definition to the word (e.g. true is interpreted as the true constant and not widened as boolean). It is pure TypeScript and has zero impact on the compiled code.
Why use json-schema-to-ts ?
There are many librearies out there like zod (type first validation) our . Feel free to check them, they may suit your needs better!
However, if you prefer to stick to JSON schemas (and there are good reasons to do so) AND if you can define your schemas in TS (the as const
statement doesn't work on JSON imports... for now !), then json-schema-to-ts
is made for you !
Doesn't impact, what's lighter than a dev-only library ?
Let's face it, making small errors in JSON schemas is easy!
Which has the benefits:
- 0 impact on compiled code: What's lighter than a dev dependancy ?
- 📝 Reduce redundancy: Writing less code is always nice!
- 🤝 Enforce consistency: Spot typos, string instead of enum or confusing additionalItems and additionalProperties...
- ⚡️ Be right first-time: With instantaneous feedback given by TS !
- Types are easier to read than shemas. Help you write better schemas
Helps you write complex schemas !
For instance, is it obvious that the following schema can never be validated ?
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;
Well, it is with FromSchema
!
type Address = FromSchema<typeof addressSchema>;
Extensively tested against the ajv library. Trust!
Docs
Installation
npm install --save-dev json-schema-to-ts
yarn add --dev json-schema-to-ts
Use cases
Const
const fooSchema = {
const: "foo",
} as const;
type Foo = FromSchema<typeof fooSchema>;
Enums
const enumSchema = {
enum: [true, 42, { foo: "bar" }],
} as const;
type Enum = FromSchema<typeof enumSchema>;
You can also go full circle with typescript enums
.
enum Food {
Pizza = "pizza",
Taco = "taco",
Fries = "Fries",
}
const enumSchema = {
enum: Object.values(Food),
} as const;
type Enum = FromSchema<typeof enumSchema>;
Litterals
const litteralSchema = {
type: "null",
} as const;
type Litteral = FromSchema<typeof litteralSchema>;
const litteralsSchema = {
type: ["null", "string"],
} as const;
type Litterals = FromSchema<typeof litteralsSchema>;
(For object
and array
types, properties like required
or additionalItems
will work 🙌)
Arrays
const arraySchema = {
type: "array",
items: { type: "string" },
} as const;
type Array = FromSchema<typeof arraySchema>;
Tuples
const tupleSchema = {
type: "array",
items: [{ type: "boolean" }, { type: "string" }],
} as const;
type Tuple = FromSchema<typeof tupleSchema>;
FromSchema
supports the additionalItems
specifications:
const tupleSchema = {
type: "array",
items: [{ type: "boolean" }, { type: "string" }],
additionalItems: false,
} as const;
type Tuple = FromSchema<typeof tupleSchema>;
const tupleSchema = {
type: "array",
items: [{ type: "boolean" }, { type: "string" }],
additionalItems: { type: "number" },
} as const;
type Tuple = FromSchema<typeof tupleSchema>;
...as well as the minItems
and maxItems
specifications:
const tupleSchema = {
type: "array",
items: [{ type: "boolean" }, { type: "string" }],
minItems: 1,
maxItems: 2,
} as const;
type Tuple = FromSchema<typeof tupleSchema>;
Additional items will only work if Typescript's strictNullChecks
option is activated
Objects
const objectSchema = {
type: "object",
properties: {
foo: { type: "string" },
bar: { type: "number" },
},
required: ["foo"],
} as const;
type Object = FromSchema<typeof objectSchema>;
FromSchema
partially supports the additionalProperties
and patternProperties
specifications:
additionalProperties
can be used to deny additional items.
const closedObjectSchema = {
...objectSchema,
additionalProperties: false,
} as const;
type Object = FromSchema<typeof additionalPropertiesSchema>;
- Used on their own,
additionalProperties
and/or patternProperties
can be used to type unnamed properties.
const objectSchema = {
type: "object",
additionalProperties: {
type: "boolean",
},
patternProperties: {
"^S": { type: "string" },
"^I": { type: "integer" },
},
} as const;
type Object = FromSchema<typeof typedValuesSchema>;
- However, when used in combination with the
properties
keyword, extra properties will always be typed as unknown
to avoid conflicts.
AnyOf
const anyOfSchema = {
anyOf: [
{ type: "string" },
{
type: "array",
items: { type: "string" },
},
],
} as const;
type AnyOf = FromSchema<typeof fooSchema>;
FromSchema
will correctly infer factored schemas:
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>;
OneOf
Because TypeScript doesn't have refinment types, oneOf
will behave strictly the same as anyOf
.
const catSchema = {
type: "object",
oneOf: [
{ properties: { name: { type: "string" } }, required: ["name"] },
{ properties: { color: { enum: ["black", "brown", "white"] } } },
],
} as const;
type Cat = FromSchema<typeof catSchema>;
const invalidCat: Cat = { name: "Garfield" };
AllOf
...Coming soon 😃
If/Else
...Coming soon 😃