Giter VIP home page Giter VIP logo

json-schema-to-ts's Introduction

If you use this repo, star it ✨

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>;
// => Will infer the same type as above

Schemas can even be nested, as long as you don't forget the as const statement:

const catSchema = { ... } as const;

const petSchema = {
  anyOf: [dogSchema, catSchema],
} as const;

type Pet = FromSchema<typeof petSchema>;
// => Will work πŸ™Œ

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.

If you don't mind impacting the compiled code, you can use the asConst util, which simply returns the schema while narrowing its inferred type.

import { asConst } from "json-schema-to-ts";

const dogSchema = asConst({
  type: "object",
  ...
});

type Dog = FromSchema<typeof dogSchema>;
// => Will work as well πŸ™Œ

Since TS 4.9, you can also use the satisfies operator to benefit from type-checking and autocompletion:

import type { JSONSchema } from "json-schema-to-ts";

const dogSchema = {
  // Type-checked and autocompleted πŸ™Œ
  type: "object"
  ...
} as const satisfies JSONSchema

type Dog = FromSchema<typeof dogSchema>
// => Still work πŸ™Œ

You can also use this with JSDocs by wrapping your schema in /** @type {const} @satisfies {import('json-schema-to-ts').JSONSchema} */ (...) like:

const dogSchema = /** @type {const} @satisfies {import('json-schema-to-ts').JSONSchema} */ ({
  // Type-checked and autocompleted πŸ™Œ
  type: "object"
  ...
})

/** @type {import('json-schema-to-ts').FromSchema<typeof dogSchema>} */
const dog = { ... }

Why use json-schema-to-ts?

If you're looking for runtime validation with added types, libraries like yup, zod or runtypes may suit your needs while being easier to use!

On the other hand, JSON schemas have the benefit of being widely used, more versatile and reusable (swaggers, APIaaS...).

If you prefer to stick to them and can define your schemas in TS instead of JSON (importing JSONs as const is not available yet), then json-schema-to-ts is made for you:

  • βœ… Schema validation FromSchema raises TS errors on invalid schemas, based on DefinitelyTyped's definitions
  • ✨ No impact on compiled code: json-schema-to-ts only operates in type space. And after all, what's lighter than a dev-dependency?
  • 🍸 DRYness: Less code means less embarrassing typos
  • 🀝 Real-time consistency: See that string that you used instead of an enum? Or this additionalProperties you confused with additionalItems? Or forgot entirely? Well, json-schema-to-ts does!
  • πŸ”§ Reliability: FromSchema is extensively tested against AJV, and covers all the use cases that can be handled by TS for now*
  • πŸ‹οΈβ€β™‚οΈ Help on complex schemas: Get complex schemas right first time with instantaneous typing feedbacks! For instance, it's not obvious 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;

But it is with FromSchema!

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

# 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.

Use cases

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"}

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>;
// => Food

Primitive types

const primitiveTypeSchema = {
  type: "null", // "boolean", "string", "integer", "number"
} as const;

type PrimitiveType = FromSchema<typeof primitiveTypeSchema>;
// => null, boolean, string or number
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

const nullableSchema = {
  type: "string",
  nullable: true,
} as const;

type Nullable = FromSchema<typeof nullableSchema>;
// => string | null

Arrays

const arraySchema = {
  type: "array",
  items: { type: "string" },
} as const;

type Array = FromSchema<typeof arraySchema>;
// => string[]

Tuples

const tupleSchema = {
  type: "array",
  items: [{ type: "boolean" }, { type: "string" }],
} as const;

type Tuple = FromSchema<typeof tupleSchema>;
// => [] | [boolean] | [boolean, string] | [boolean, string, ...unknown[]]

FromSchema supports the additionalItems keyword:

const tupleSchema = {
  type: "array",
  items: [{ type: "boolean" }, { type: "string" }],
  additionalItems: false,
} as const;

type Tuple = FromSchema<typeof tupleSchema>;
// => [] | [boolean] | [boolean, string]
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[]]

...as well as the minItems and maxItems keywords:

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

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; }

Defaulted properties (even optional ones) will be set as required in the resulting type. You can turn off this behavior by setting the keepDefaultedPropertiesOptional option to true:

const defaultedProp = {
  type: "object",
  properties: {
    foo: { type: "string", default: "bar" },
  },
  additionalProperties: false,
} as const;

type Object = FromSchema<typeof defaultedProp>;
// => { foo: string; }

type Object = FromSchema<
  typeof defaultedProp,
  { keepDefaultedPropertiesOptional: true }
>;
// => { foo?: string; }

FromSchema partially supports the additionalProperties and patternProperties keywords:

  • additionalProperties can be used to deny additional properties.
const closedObjectSchema = {
  ...objectSchema,
  additionalProperties: false,
} as const;

type Object = FromSchema<typeof closedObjectSchema>;
// => { foo: string; bar?: number; }
  • Used on their own, additionalProperties and/or patternProperties can be used to type unnamed properties.
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 }
  • However, when used in combination with the properties keyword, extra properties will always be typed as unknown to avoid conflicts.

Combining schemas

AnyOf

const anyOfSchema = {
  anyOf: [
    { type: "string" },
    {
      type: "array",
      items: { type: "string" },
    },
  ],
} as const;

type AnyOf = FromSchema<typeof anyOfSchema>;
// => string | string[]

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>;
// => {
//  [x:string]: unknown;
//  bool: boolean;
//  str: string;
// } | {
//  [x:string]: unknown;
//  bool: boolean;
//  num?: number;
// }

OneOf

FromSchema will parse the oneOf keyword in the same way 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>;
// => {
//  [x: string]: unknown;
//  name: string;
// } | {
//  [x: string]: unknown;
//  color?: "black" | "brown" | "white";
// }

// => Error will NOT be raised 😱
const invalidCat: Cat = { name: "Garfield" };

AllOf

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

Exclusions require heavy computations, that can sometimes be aborted by Typescript and end up in any inferred types. For this reason, they are not activated by default: You can opt-in with the parseNotKeyword option.

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]
const primitiveTypeSchema = {
  not: {
    type: ["array", "object"],
  },
} as const;

type PrimitiveType = FromSchema<
  typeof primitiveTypeSchema,
  { parseNotKeyword: true }
>;
// => null | boolean | number | string

In objects and tuples, the exclusion will propagate to properties/items if it can collapse on a single one.

// πŸ‘ 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" }
// ❌ 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" }

As some actionable keywords are not yet parsed, exclusions that resolve to never are granted the benefit of the doubt and omitted. For the moment, FromSchema assumes that you are not crafting unvalidatable exclusions.

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"

Also, keep in mind that TypeScript misses refinment types:

const goodLanguageSchema = {
  type: "string",
  not: {
    enum: ["Bummer", "Silly", "Lazy sod !"],
  },
} as const;

type GoodLanguage = FromSchema<
  typeof goodLanguageSchema,
  { parseNotKeyword: true }
>;
// => string

If/Then/Else

For the same reason as the Not keyword, conditions parsing is not activated by default: You can opt-in with the parseIfThenElseKeywords option.

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

const userSchema = {
  type: "object",
  properties: {
    name: { $ref: "#/definitions/name" },
    age: { $ref: "#/definitions/age" },
  },
  required: ["name", "age"],
  additionalProperties: false,
  definitions: {
    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

Unlike run-time validator classes like AJV, TS types cannot withhold internal states. Thus, they cannot keep any identified schemas in memory.

But you can hydrate them via the references option:

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

You can specify deserialization patterns with the deserialize option:

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;
// }

Extensions

If you need to extend the JSON Schema spec with custom properties, use the ExtendedJSONSchema and FromExtendedSchema types to benefit from json-schema-to-ts:

import type { ExtendedJSONSchema, FromExtendedSchema } from "json-schema-to-ts";

type CustomProps = {
  numberType: "int" | "float" | "bigInt";
};

const bigIntSchema = {
  type: "number",
  numberType: "bigInt",
  // πŸ‘‡ Ensures mySchema is correct (includes extension)
} as const satisfies ExtendedJSONSchema<CustomProps>;

type BigInt = FromExtendedSchema<
  CustomProps,
  typeof bigIntSchema,
  {
    // πŸ‘‡ Works very well with the deserialize option!
    deserialize: [
      {
        pattern: {
          type: "number";
          numberType: "bigInt";
        };
        output: bigint;
      },
    ];
  }
>;

Typeguards

You can use FromSchema to implement your own typeguard:

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 πŸ™Œ
}

If needed, you can provide FromSchema options and additional validation options to the Validator type:

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 => { ... };

json-schema-to-ts also exposes two helpers to write type guards. They don't impact the code that you wrote (they simply return it), but turn it into type guards.

You can use them to wrap either validators or compilers.

Validators

A validator is a function that receives a schema plus some data, and returns true if the data is valid compared to the schema, false otherwise.

You can use the wrapValidatorAsTypeGuard helper to turn validators into type guards. Here is an implementation with ajv:

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 πŸ™Œ
}

If needed, you can provide FromSchema options and additional validation options as generic types:

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

A compiler is a function that takes a schema as an input and returns a data validator for this schema as an output.

You can use the wrapCompilerAsTypeGuard helper to turn compilers into type guard builders. Here is an implementation with ajv:

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 πŸ™Œ
}

If needed, you can provide FromSchema options, additional compiling and validation options as generic types:

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);

Frequently Asked Questions

json-schema-to-ts's People

Contributors

azizghuloum avatar dariocravero avatar dependabot[bot] avatar fargito avatar github-actions[bot] avatar javivelasco avatar ngruychev avatar stalniy avatar thomasaribart avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

json-schema-to-ts's Issues

Question: is there a way to generate interfaces instead of types?

I want to use type-inheritance and if I get this right, you can only do this with interfaces not with types.

Something like this for example:

interface BasicAddress {
  name?: string;
  street: string;
  city: string;
  country: string;
  postalCode: string;
}

interface AddressWithUnit extends BasicAddress {
  unit: string;
}

But because FromSchema<typeof foo> returns a type rather than an interface how would I go about and implement inheritance with this lib?

Recursive ref issue

Taking this file as an example:

import { O } from "ts-toolbelt"
import { FromSchema } from "json-schema-to-ts"

export const leafTypeSchema = {
  type: "string",
  enum: ["string", "number", "boolean"],
} as const
export type LeafType = FromSchema<typeof leafTypeSchema>

export const objTypeSchema = {
  type: "string",
  const: "object",
} as const
export type ObjType = FromSchema<typeof objTypeSchema>

export const leafSchema = {
  type: "object",
  additionalProperties: false,
  properties: {
    id: { type: "string", format: "uuid" },
    key: { type: "string" },
    type: leafTypeSchema,
    name: { type: "string" },
  },
  required: ["id", "key", "type", "name"],
} as const
export type Leaf = FromSchema<typeof leafSchema>

export const objSchema = {
  $id: "objSchema",
  type: "object",
  additionalProperties: false,
  properties: {
    id: { type: "string", format: "uuid" },
    key: { type: "string" },
    type: objTypeSchema,
    name: { type: "string" },
    fields: { type: "array", items: { anyOf: [{ $ref: "#" }, leafSchema] } },
  },
  required: ["id", "key", "type", "name", "fields"],
} as const
export type ObjRaw = FromSchema<typeof objSchema>


export const fieldTypeSchema = {
  anyOf: [leafTypeSchema, objTypeSchema],
} as const
export type FieldType = FromSchema<typeof fieldTypeSchema>

export const fieldSchema = {
  anyOf: [objSchema, leafSchema],
} as const
export type Field = Obj | Leaf

export type Obj = O.Update<ObjRaw, "fields", Array<Field>>

I'm seeing this:
Type of property 'fields' circularly references itself in mapped type '{ id: { type: "primitive"; value: string; isSerialized: false; deserialized: never; }; key: { type: "primitive"; value: string; isSerialized: false; deserialized: never; }; type: { type: "const"; value: "object"; isSerialized: false; deserialized: never; }; name: { ...; }; fields: _$Array<...>; }'. (tsserver 2615)

This was working prior to an update from 1.6 to latest (2.5.5). Recommendation on how to resolve?

Importing JSON from file fails

Hi,

I have a file lets say named myJsonSchema.json and it contains:

{
  "name": "MySchema",
  "type": "object",
  "properties": {
    "foo": {
      "type": "string"
    },
    "bar": {
      "type": "string"
    },
  },
  "required": ["foo"],
  "additionalProperties": false
}

I then import that file and try to use it like so:

import mySchema from './myJsonSchema.json';

type MySchema = FromSchema<typeof mySchema>;

And I get this error:

TS2344: Type '{ name: string; type: string; properties: { foo: { type: string; }; bar: { type: string; }; }; required: string[]; additionalProperties: boolean; }' does not satisfy the constraint 'JSONSchema'. Β Β Type '{ name: string; type: string; properties: { foo: { type: string; }; bar: { type: string; }; }; required: string[]; additionalProperties: boolean; }' is not assignable to type '{ readonly type?: "string" | "number" | "boolean" | "object" | "any" | "array" | "null" | "integer" | readonly JSONSchema6TypeName[] | undefined; readonly allOf?: readonly (boolean | { ...; })[] | undefined; ... 35 more ...; readonly not?: unknown; }'. Β Β Β Β Types of property 'type' are incompatible. Β Β Β Β Β Β Type 'string' is not assignable to type '"string" | "number" | "boolean" | "object" | "any" | "array" | "null" | "integer" | readonly JSONSchema6TypeName[] | undefined'.

I believe this is happening because the JSON is not an object literal that can be const asserted.

Is there any way to make this work without having to define object literals? I think it is a pretty common use case to store your JSON schemas in .json files as this makes them accessible to other tools (like API Gateway validation and Swagger doc generation).

Is there a way to tell typescript the schema of nested field ?

I have two schema, you can look at the personSchema and animalSchema
There is 2 type too.
What i want is that the instance filed has People type.
How can i tell that animal.instance has type Person instead of using cast (please take a look at getPerson function)

import { FromSchema } from "json-schema-to-ts";

const personSchema = {
  type: "object",
  properties: {
    national: {
      type: "string",
    },
  },
} as const;

const animalSchema = {
  type: "object",
  properties: {
    kind: {
      type: "string",
    },
    instance: personSchema,
  },
} as const;

type Person = FromSchema<typeof personSchema>;
type Animal = FromSchema<typeof animalSchema>;

function getPerson(animal: Animal): Person {
  // return animal.instance;
  return animal.instance as Person;
}

getPerson({
  kind: "human",
  instance: {
    national: "eu",
  },
});

Thank you for a very helpful package!

Support for MongoDB ObjectId field

I use json schemas to validate my mongodb collections, and convert them to typescript types with json-schema-to-ts. Everything works fantastic.

The problem I have is that mongodb saves its ID fields as ObjectId objects, not plain strings. With the help of the deserialize option I could easily map the value to the ObjectId type like this:

type Task = FromSchema<typeof TaskSchema, {
    deserialize: [{
        pattern: {
            type: 'string',
            description: 'objectId'
        },
        output: ObjectId,
    }],
}>;

The problem is that in order to be a valid validation schema, I cannot assign the type string to an id field. Mongodb forces all id fields to have the type bsonType: 'objectId' and to not be just simple string fields. Since the attribute bsonType is not part of the valid json schema definition, the FromSchema<> conversion obviously throws an error. Is there a way I can circumvent this problem? Maybe extending the JSONSchema type definitions?

I hope there is a solutions, since I really want to work with json-schema-to-ts because of its many advantages.

When the schema is typed as JSONSchema, FromSchema<typeof schema> is never

Hello,

The newly created type from a schema is never if the schema is typed as JSONSchema.

Let me demonstrate with an object example provided in the README.MD

When typed as JSONSchema

In the following example, I denote objectSchema variable as JSONSchema, and it is indeed helpful when I am typing properties.
However, when I declare Object using objectSchema, its value is never.

import type { FromSchema, JSONSchema } from "json-schema-to-ts"

const objectSchema: JSONSchema = {
      type: "object",
      properties: {
            foo: { type: "string" },
            bar: { type: "number" },
      },
      required: ["foo"],
} as const;
type Object = FromSchema<typeof objectSchema>;

Here you can see the type when I hover over Object:
image


When NOT typed as JSONSchema

The very same example, however, works when I don't annotate objectSchema.

import type { FromSchema } from "json-schema-to-ts"

const objectSchema = {
      type: "object",
      properties: {
            foo: { type: "string" },
            bar: { type: "number" },
      },
      required: ["foo"],
} as const;
type Object = FromSchema<typeof objectSchema>;

image

I'm really not sure why, and I'm confused as FromSchema is already defined as a generic expecting JSONSchema type, however, resulting in never when a value typed as JSONSchema.

export declare type FromSchema<S extends JSONSchema> = Resolve<ParseSchema<DeepWriteable<S>>>;

Upgrading to 1.6.x causes "Type instantiation is excessively deep and possibly infinite" errors

Hi there, thanks for the amazing work 🀩 !
I recently updated to latest version of json-schema-to-ts and was welcome with a TS error message Type instantiation is excessively deep and possibly infinite. All information relative to the issue are detailed in fredericbarthelet/typebridge#8

At the moment, I locked version 1.5.x. Could you help on the matter ? Do you know which dependency upgrade from v1.6 might cause this issue ?

Thanks !

Typescript Error when using allOf

This is my schema:

{
	title: 'Website Meta Tags Body',
	type: 'object',
	properties: {
		WebsiteID: {
			type: 'integer',
			min: 0
		},
		Body: {
			description: 'Our required Body is Array of object.',
			type: 'array',
			items: {
				type: 'object',
				description: 'In this object we have Type key and based on Type key we have 2 more keys MetaTagID and Body, So we added conditions for that.',
				properties: {
					Type: {
						type: 'string',
						enum: [
							'INSERT',
							'UPDATE',
							'DELETE'
						]
					},
					MetaTagID: {
						type: 'integer',
						min: 1
					},
					Body: {
						type: 'object',
						properties: {
							Name: {
								type: 'string',
								minLength: 1
							},
							Content: {
								type: 'string',
								minLength: 1
							}
						},
						required: [
							'Name',
							'Content'
						]
					}
				},
				required: [
					'Type'
				],
				allOf: [
					{
						if: {
							properties: {
								Type: {
									const: 'INSERT'
								}
							},
							required: [
								'Type'
							]
						},
						then: {
							required: [
								'Body'
							]
						}
					},
					{
						if: {
							properties: {
								Type: {
									const: 'UPDATE'
								}
							},
							required: [
								'Type'
							]
						},
						then: {
							required: [
								'Body',
								'MetaTagID'
							]
						}
					},
					{
						if: {
							properties: {
								Type: {
									const: 'DELETE'
								}
							},
							required: [
								'Type'
							]
						},
						then: {
							required: [
								'MetaTagID'
							]
						}
					}
				]
			},
			minItems: 1
		}
	},
	required: [
		'WebsiteID',
		'Body'
	],
	additionalProperties: false
}

When used FromSchema, Typescript gives some long error, unable to understand the exact error, but after removing allOf it's working properly.

Does anyone of you have any workaround for this issue?

Error when using array in examples

It seems that using an array anywhere as part of examples produces a compilation error.

const schema = {
    type: "array",
    items: {
        type: "string"
    },
    examples: [
        ["test"]
    ]
} as const;

type Example = FromSchema<typeof schema>;

This produces a compilation error:

Type '{ readonly type: "array"; readonly items: { readonly type: "string"; }; readonly examples: readonly [readonly ["test"]]; }' does not satisfy the constraint 'JSONSchema'.
  Type '{ readonly type: "array"; readonly items: { readonly type: "string"; }; readonly examples: readonly [readonly ["test"]]; }' is not assignable to type '{ readonly type?: JSONSchema6TypeName | readonly JSONSchema6TypeName[] | undefined; readonly items?: boolean | { readonly $id?: string | undefined; readonly $ref?: string | undefined; ... 35 more ...; readonly format?: string | undefined; } | readonly (boolean | { ...; })[] | undefined; ... 35 more ...; readonly not...'.
    Types of property 'examples' are incompatible.
      Type 'readonly [readonly ["test"]]' is not assignable to type 'readonly (string | number | boolean | { readonly [x: string]: string | number | boolean | ... | { readonly [x: number]: string | number | boolean | ... | ... | null; readonly length: number; readonly toString: {}; readonly toLocaleString: {}; ... 30 more ...; readonly [Symbol.unscopables]: {}; } | null; } | { ...; }...'.
        Type 'readonly ["test"]' is not assignable to type 'string | number | boolean | { readonly [x: string]: string | number | boolean | ... | { readonly [x: number]: string | number | boolean | ... | ... | null; readonly length: number; readonly toString: {}; readonly toLocaleString: {}; ... 30 more ...; readonly [Symbol.unscopables]: {}; } | null; } | { ...; } | null'.
          Type 'readonly ["test"]' is not assignable to type '{ readonly [x: number]: string | number | boolean | { readonly [x: string]: string | number | boolean | ... | ... | null; } | ... | null; readonly length: number; readonly toString: {}; readonly toLocaleString: {}; ... 30 more ...; readonly [Symbol.unscopables]: {}; }'.ts(2344)

Another example. This works fine:

const schema = {
    additionalProperties: true,
    type: "object",
    properties: {
        foo: {
            type: "string"
        }
    },
    examples: [
        {
            foo: "bar",
            someInt: 1
        }
    ]

} as const;

type Example = FromSchema<typeof schema>;

But if I add an array to the example I get a similar compilation error as above:

const schema = {
    additionalProperties: true,
    type: "object",
    properties: {
        foo: {
            type: "string"
        }
    },
    examples: [
        {
            foo: "bar",
            someInt: 1,
            someArray: [] // <-- Type 'readonly []' is not assignable to type '{ readonly [x: number]: string | number | boolean | { readonly [x: string]: string | number | boolean | ... | ... | null; } | ... | null; readonly length: number; readonly toString: {}; readonly toLocaleString: {}; ... 30 more ...; readonly [Symbol.unscopables]: {}; }'.ts(2344)
        }
    ]

} as const;

type Example = FromSchema<typeof schema>;

[bug] anyOf/union types not working with nulls

Summary

anyOf and arrays of types in JSON schemas does not appear to behave correctly with nulls. Instead of unioning the type with null, nothing seems to happen.

Details

json-schema-to-ts version: 1.6.4
typescript version: 4.4.3

Example

import { FromSchema } from 'json-schema-to-ts';
const mySchema = { anyOf: [{ type: "string" }, { type: "null" }] } as const
type MyType = FromSchema<typeof mySchema>
import { FromSchema } from 'json-schema-to-ts';
const mySchema = { type: ["string", "null"] } as const
type MyType = FromSchema<typeof mySchema>

In both these examples, MyType should be string | null, but is instead string

How to write the type definitions to files?

Hi, thanks for this library.

I'm wondering if there's a way for me to output the FromSchema definition result into a file ?

I only need to transform the json schema and write it to my own typescript file.

Using a dictionary with `$ref` to reference predefined types

First, your type mappings here are next level and as much as I've tried to follow along, I'll admit that I've gotten lost. That being said, I wonder if the following approach would work.

Your example of containing references creates a type that contains no reference to the original type:

const petSchema = {
  anyOf: [dogSchema, catSchema],
} as const;

The result is a unique type.

Consider the following example from the JSON Schema site (https://json-schema.org/understanding-json-schema/structuring.html):

{
  "$id": "https://example.com/schemas/address",

  "type": "object",
  "properties": {
    "street_address": { "type": "string" },
    "city": { "type": "string" },
    "state": { "type": "string" }
  },
  "required": ["street_address", "city", "state"]
}
{
  "$id": "https://example.com/schemas/customer",
  "type": "object",
  "properties": {
    "first_name": { "type": "string" },
    "last_name": { "type": "string" },
    "shipping_address": { "$ref": "/schemas/address" },
    "billing_address": { "$ref": "/schemas/address" }
  },
  "required": ["first_name", "last_name", "shipping_address", "billing_address"]
}

If we used a dictionary to map the $ref values, wouldn't the following be possible.

const addressSchema = { ... };
type Address = FromSchema<typeof AddressSchema>;

const customerSchema = { ... };
type customerSchemaDependencies = {
  "/schemas/address": Address
}
type Customer = FromSchemaWithDeps<typeof customerSchema, customerSchemaDependencies>;
// result type has reference to actual Address; instead of a copy

My little tests suggest that a dictionary approach for this would be possible:

type A1 = {};
type A2 = {};

type Deps = {
  foo: A1;
  bar: A2;
}

type Remapper<T, U> = {
  [P in keyof T]: T[P] extends keyof U ? U[T[P]] : T[P];
}

const Test = {
  a1: 'foo',
  a2: 'bar'
} as const

type Remapped = Remapper<typeof Test, Deps>;
/*
result:
type Remapped = {
    readonly a1: A1;
    readonly a2: A2;
}
*/

Is it possible to update FromSchema to use a dictionary in this way or would we need a new mapping type?
Do you have an idea where that mapping could happen? I'd be willing to help, but as I mentioned before I found source rather complex.

Using definition results in unknown type

Version: 1.6.4

Take the following simple schema

export const simpleSchema = {
  $schema: 'https://json-schema.org/draft-2020-12/schema',
  type: 'object',
  properties: {
    exampleObject: {
      type: 'object',
      properties: {
        name: {
          $ref: '#/definitions/example'
        }
      },
      required: ['name'],
      additionalProperties: false
    }
  },
  required: [
    'exampleObject'
  ],
  additionalProperties: false,
  definitions: {
    example: {
      type: 'string'
    }
  }
} as const

When converting this to TS using FromSchema results in the name property being an unknown type.

import { simpleSchema } from './simpleSchema'
type SimpleSchema = FromSchema<typeof simpleSchema>

image

Hoping someone can shed some light on this for me. Hoping I'm making a simple mistake somewhere

How to type input for `FromSchema` correctly?

I have the following type of a helper function. I am trying to use JSONSchema here to fulfil the constraint of FromSchema.

type MkMiddy = <Res>(handler: Handler, inputSchema?: JSONSchema) => Lambda.ValidatedAPIGatewayProxyHandlerV2<FromSchema<typeof inputSchema>, Res>

However, Typescript is complaining for this:

error TS2589: Type instantiation is excessively deep and possibly infinite.

Based on the doc, https://github.com/ThomasAribart/json-schema-to-ts/blob/HEAD/documentation/FAQs/can-i-assign-jsonschema-to-my-schema-and-use-fromschema-at-the-same-time.md, it is incorrect to use both types together, so what type should I provide there instead? unknown doesn't work

Question: what is the performance impact of implementing this in the typing system?

Hi @ThomasAribart, thanks for your awesome work, json-schema-to-ts looks very interesting!

I was wondering though, what performance impact can we expect implementing this in the typing system? A lot of TS logic needs to happen on the fly for inferring the schema TS types, and my worry is that when codebases grow, the TypeScript language server might take a long time to startup, and we'll start feeling in in our editing experience. But maybe it's all super fast and little work compared to all other things TS has to do on a codebase.

I imagine a few use cases where this matters:

  • opening your editor, lang server starts, opening a file that uses a FromSchema type or two from other files
  • opening your editor, lang server starts, opening a file with many/all schema's in your app
  • building your project with tsc

I'm now using a (more complex) build time solution, but if json-schema-to-ts has an insignificant perf impact it would make things easier and more elegant. Curious about your thoughts on this!

schema object without a type attribute

Use case: does-json-schema-to-ts-work-on-json-file-schemas

Look at: schema-object-without-a-type-attribute-in-swagger-2-0

const Account = {
	properties: {
		balance: {
			type: 'string',
			description: 'balance in unit WEI, presented with hex string',
			example: '0x47ff1f90327aa0f8e',
		},
		energy: {
			type: 'string',
			description: 'energy in uint WEI, presented with hex string',
			example: '0xcf624158d591398',
		},
		hasCode: {
			type: 'boolean',
			description: 'whether the account has code',
			example: false,
		},
	},
} as const;
type Dog = FromSchema<typeof Account>;

// Now: type Dog = unknown
// Expected: type Dog = { balance, ... }

Excessive stack depth comparing types 'ParseMixedSchema<?, T>' and 'ParseMixedSchema<?, T>'.

Hello and thank you for this awesome library! (my little project needs TS safety and serializable schemas, so I would be screwed without json-schema-to-ts!)

This error frequently appears in my codebase when TS performs deep inference with a FromSchema type:

Screen Shot 2022-04-01 at 2 59 05 PM

Heres a Youtube video where I demo this code and we see the issue: https://youtu.be/O2xGGEOYYyI?t=438

Somehow TS gets tripped up, and I only see this error when working with JSON-schema-to-ts codebases.

I have not found any way to "TS-Ignore" this, without causing breaks to useful type safety. As you can see in the screenshot, the "number" schema is being TS-ified correctly.

This example is open source here: https://github.com/zerve-app/zerve/blob/main/apps/demo-server/DemoServer.ts#L8-L10

Any advice or help would be greatly appreciated! ❀️

Can deserialization patterns use the property name?

I'm trying to use a custom deserialization to type check CSS in my JSON objects.

I'm using a CSS type fromcsstype:

import type * as CSS from "csstype"; 
type CustomStyle = CSS.Properties<string | number>;

And have the CSS as a property called "customStyle" in my schemas:

const schemaWithCustomStyle =  {
  type: "object",
  properties: {
    customStyle: {
      type: "object",
    },
  },
  additionalProperties: false,
} as const;

The type I would like to get from this is something like

type TypeWithCustomStyle = {
    customStyle?: CustomStyle;
}

However since there's not enough information in the type to match a pattern:

type TypeWithCustomStyle = FromSchema<
  typeof schemaWithCustomStyle,
  {
    deserialize: [
      {
        pattern: {
          type: "object";
        };
        output: CustomStyle;
      }
    ];
  }
>;

matches the outer schema:

type TypeWithCustomStyle = CSS.Properties<string | number, string & {}>

Is there a way to pattern match based on the name of the property, e.g.

type TypeWithCustomStyle = FromSchema<
  typeof schemaWithCustomStyle,
  {
    deserialize: [
      {
        pattern: {
          propertyName: "customStyle";
        };
        output: CustomStyle;
      }
    ];
  }
>;

type TypeWithCustomStyle = {
    customStyle?: CustomStyle;
}

I took a look at the deserialize.ts file:

type RecurseOnDeserializationPatterns<
  S extends JSONSchema7,
  P extends DeserializationPattern[],
  R = M.Any
> = {
  stop: R;
  continue: RecurseOnDeserializationPatterns<
    S,
    L.Tail<P>,
    S extends L.Head<P>["pattern"]
      ? M.$Intersect<M.Any<true, L.Head<P>["output"]>, R>
      : R
  >;
}[P extends [any, ...any[]] ? "continue" : "stop"];

but don't understand how exactly it's working.

`Unexpected token 'export'` when exporting `wrapValidatorAsTypeguard` with a ts-node build

Hello, thanks for writing this fantastic library, it has been useful so far!

I'm getting the following error when building with ts-node:

$ cross-env NODE_ENV=development nodemon --exec ts-node -r dotenv-flow/config -r tsconfig-paths/register src/index.ts | pino-pretty -c
[nodemon] 2.0.19
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: ts,json
[nodemon] starting `ts-node -r dotenv-flow/config -r tsconfig-paths/register src/index.ts`
/home/lcsmuller/PF/quickstart-nodejs-rest/node_modules/json-schema-to-ts/lib/index.js:1
export { wrapCompilerAsTypeGuard, wrapValidatorAsTypeGuard, } from "json-schema-to-ts/lib/type-guards";
^^^^^^

SyntaxError: Unexpected token 'export'
    at Object.compileFunction (node:vm:352:18)
    at wrapSafe (node:internal/modules/cjs/loader:1033:15)
    at Module._compile (node:internal/modules/cjs/loader:1069:27)
    at Module._extensions..js (node:internal/modules/cjs/loader:1159:10)
    at Object.require.extensions.<computed> [as .js] (/home/lcsmuller/PF/quickstart-nodejs-rest/node_modules/ts-node/src/index.ts:1361:43)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Module.require (node:internal/modules/cjs/loader:1005:19)
    at require (node:internal/modules/cjs/helpers:102:18)
    at Object.<anonymous> (/home/lcsmuller/PF/quickstart-nodejs-rest/src/common/schemas/utils/helpers.ts:3:1)
[nodemon] app crashed - waiting for file changes before starting...

This error only seems to trigger when I attempt to export a method that comes from a .js file (wrapValidatorAsTypeguard or wrapCompilerAsTypeguard)... It appears that ts-node cannot support export and import syntax by default, I think an easy fix for this would be targeting es5 instead (unless there is a rationale for not doing so). If you are busy, I can give it a try to write the PR.

Error: Excessive stack depth comparing types 'ParseMixedSchema<?, T>' and 'ParseMixedSchema<?, T>'.ts(2321)

As of version 1.6.5, I now get this error in several places in my code.

Excessive stack depth comparing types 'ParseMixedSchema<?, T>' and 'ParseMixedSchema<?, T>'.ts(2321)

It comes from this:

import type { FromSchema } from 'json-schema-to-ts';
import createHttpError, { NamedConstructors } from 'http-errors';
import { URLSearchParams } from 'url';

export type ValidatedAPIGatewayProxyEvent<S> = APIGatewayProxyEvent &
  FromSchema<S>;

which I adopted from the Serverless Framework TS template.

I create a JS/JSON API Gateway schema, e.g.:

const schema = {
  type: 'object',
  properties: {
    body: {
      type: 'object',
      properties: {
        coupon_id: { type: 'string' },
        end_of_term: { type: 'boolean' },
        plan_id: { type: 'string' },
        subscription_id: { type: 'string' },
        reactivate: { type: 'boolean' },
        first_payment: { type: 'boolean' },
      },
      required: ['end_of_term', 'plan_id', 'subscription_id'],
      additionalProperties: false,
    },
  },
} as const;

and pass it to Sentry's wrapHandler:

export const handler = Sentry.AWSLambda.wrapHandler<
      ValidatedAPIGatewayProxyEvent<S>,
      APIGatewayProxyResult | undefined
    >(...

This has worked fine up until the latest release.

enum: Object.keys resolves as string[] type

Hi,

const my_enum = {
  a: 1,
  b: 2
} as const

type my_enum_type=Array<keyof typeof my_enum>

const mySchema = {
  type: 'array',
  items: {
    type: 'string',
    enum: Object.keys(my_enum)
  }
} as const

type my_enum_schema = FromSchema<typeof mySchema>

my_enum_type != my_enum_schema

Maybe someone could suggest how to fix it?

if i use enum: ["a","b"] all is fine.

Create query param object from query param list

Hi! First off: thanks for all the effort in creating and maintaining this library!

I have a question that I can't seem to figure out an answer to regarding using this library with express and express-openapi, and that I was hoping you could shed some light on:

In short, I'm wondering if there is a way to create a record from query parameters via FromSchema?

A little more context: the OpenAPI spec requires you to type query parameters as a list of objects. These objects have a name, a type, description, and so forth, but it is a list of spec-like objects. Something like:

const params = [
    {
        name: 'paramName',
        schema: {
            type: 'string',
        },
        in: 'query',
    },
] as const;

For endpoints that accept JSON payloads, using FromSchema to generate types has gone very smoothly, but it seems to be a bit trickier with query parameters. Express types out the request as something like this:

        req: Request<unknown, unknown, unknown, QueryParamObject>,

where QueryParamObject is an object with query param names as keys and corresponding types.

My problem is that I'd like to convert the list of query parameters into something that FromSchema can parse, so that we get a nice generated type based on the schema, but I can't figure out how to do it.

The list is obviously not a schema in and of itself, but it's easy enough to map it so that it becomes the equivalent of a properties object on an object-type schema. However, I keep running into issues with readonly and type inference and I'm just not making any headway. Do you have any suggestions as to what I could do here?

Sorry if this doesn't actually apply to this library. I was not the one who set everything up for the project, so I'm not entirely sure how everything is wired together. I do know, though, that the FromSchema method does a lot of our type conversion and that the internals looked pretty intimidating. It may well be that this is outside of what this package is responsible for , but I thought I'd try this as my first port of call.

Thanks very much for any tips, tricks, and insights you can offer.

AJV format support ?

Hey there!
First of all Whow 🀯 So great utility plugin ! really powerfull and easy to use! you are typescript ninjas !
Great job !

Here is my issue.
I'm using AJV format like this

import addFormat from 'ajv-formats'
import {FromSchema} from 'json-schema-to-ts';

export const validateParameters = (
  parametersData: any,
  schema: any
) => {
  const ajv = new Ajv({
    allErrors: true,
    removeAdditional: 'all',
    coerceTypes: true,
    unicodeRegExp: false,
  });
  addFormat(ajv);
  ...

Here is my schema code:

{
  type: 'object',
  properties: {
 supplierInvoiceFile: {
      type: 'object',
      properties: {
        type: {type: 'string'},
        filename: {type: 'string'},
        contentType: {type: 'string'},
        content: {type: 'binary', contentMediaType: 'application/pdf'},
      },
    },
}
}

and here is the calling code :

const body = parse(event, true) as FromSchema<typeof schema>;  //error 
/**
* Type '{ readonly type: "object"; readonly properties: { readonly isTest: { readonly type: "boolean"; readonly default: false; }; 
*  readonly supplierInvoiceFile: { readonly type: "object"; readonly properties: { readonly type: { readonly type: "string"; }; readonly 
*  filename: { ...; }; readonly contentType: { ...; }; readonly...' does not satisfy the constraint 'JSONSchema'.ts(2344)
**/

As soon as the property content is equal to "binary" it seems that the FromSchema is broken.

Is this a normal behavior as the support for binary type is not provided ? Or am I doing something wrong ?

Thanks for your answer !

allOf operator and ajv-keywords support

Hi, thanks for this library.

I noticed that FromSchema utility does not work with ajv-keywords such as transform: ['trim'] for example. Seems like a bug to me.

image

Code to reproduce:

export const schema = {
  type: 'object',
  properties: {
    prop1: {
      type: 'string',
      allOf: [
        {
          transform: ['trim'],
        },
      ],
    },
  },
  required: ['prop1'],
  additionalProperties: false,
} as const;

type Schema = FromSchema<typeof schema>;

support of nullable keyword.

As we know, AJV by default support nullable: true property.
But if we try the same in FromSchema, it's not working.

Is there any way we can work with this?

Any limits on the size of the schema?

Really nice project, thank you!

Anyone tried it with large schemas and seen 1) performance degradation or 2) tsc using more relaxed typings because of the size?

`wrapCompilerAsTypeGuard` and `wrapValidatorAsTypeGuard` are not usable in Deno build

Hello! First off thanks for writing this library! It's awesome and just keeps getting better!

One thing I noticed is in the Deno build the wrapper functions cannot be used because its packaged as only type definitions and the implementations are stripped by rollup

declare const wrapCompilerAsTypeGuard: CompilerWrapper;

declare const wrapValidatorAsTypeGuard: ValidatorWrapper;

Trying to import it and use it throws

error: SyntaxError: The requested module 'https://deno.land/x/[email protected]/index.d.ts' does not provide an export named 'wrapCompilerAsTypeGuard'

I fixed it by just adding the implementation real quick and changing it to a .ts file since Deno doesnt always play nice with .d.ts files. That may not work the best with your rollup setup though and im sure you know a more compatible fix!

Schema Definitions Arn't Supported

I was attempting to define a schema definition inside my json schema and i'm not getting type validation for the definition value S. I used the Sample Input to verify against the Reference Json Schema

Sample Input

{
  "dynamodb": {
    "NewImage": {
      "id": {
        "S": "b76e5354-0000-0000-0000-17bd3d223d91"
      }
    }
  }
}

Reference Json Schema

  "definitions": {
    "string": {
      "type": "object",
      "properties": {
        "S": {
          "type": "string"
        }
      },
      "required": [
        "S"
      ]
    }
  },
  "required": [
    "dynamodb"
  ],
  "type": "object",
  "properties": {
    "dynamodb": {
      "type": "object",
      "properties": {
        "NewImage": {
          "required": [
            "id"
          ],
          "type": "object",
          "properties": {
            "id": {
              "$ref": "#/definitions/string"
            }
          }
        }
      }
    }
  }
}```

Issue with CompilerOptions.keyofStringsOnly = true

The issue

FromSchema does not infer required properties when compilerOptions.keyofStringsOnly = true.

The reason

I'm working on a project where ts compiler option keyofStringsOnly is set to true. This makes keyof WHATEVER to return string type instead of string | number | symbol. That's why when you check whether number extends keyof S["required"] on this line, it return false.

In general, keyof operator is not safe to test whether type is an array or tuple because even for objects and interfaces it returns string | number.

Also if you check the output type of type indexesOfArray = keyof string[], you will see that ts returns number and names of all
methods. Even keyof ReadonlyArray<string> because it contains numeric indexes, length and all non mutating array methods

How to fix

In order to fix the issue, you need to convert that and similar checks to check on ReadonlyArray<string> or on { length: number }

type GetRequired<S> = "required" extends keyof S
  ? S["required"] extends ReadonlyArray<string>  // <----
    ? S["required"][number]
    : never
  : never;

Document compilation performance

This looks like an amazing project! I'd love to build an app around it.

One thing I'm curious about is how slow it might make my TS compilation times as the application grows. Do you have any real-world data of compilation times in large projects? Are you aware of pathological patterns in js schemas that cause slow compilation? Or do you have a theoretical idea of how slow this could get in certain situations?

Thanks!

Inferred type is always `never`

Hey,

I am trying the example for the README and I get never as the resulting type :\

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>;
// => Dog is never

I am using Typescript 4.8 on MacOS 12.6 and this is my package.json:

{
  "name": "fastify-ts-typeprovider-demo",
  "version": "0.0.1",
  "description": "",
  "main": "src/server.js",
  "scripts": {
    "start": "nodemon",
    "build": "swc ./src -d built"
  },
  "dependencies": {
    "json-schema-to-ts": "2.5.5"
  },
  "devDependencies": {
    "@swc-node/register": "1.5.4",
    "@swc/cli": "0.1.57",
    "@swc/core": "1.3.10",
    "@types/node": "18.11.3",
    "nodemon": "2.0.20",
    "typescript": "4.8.4"
  },
  "engines": {
    "npm": ">=7.0.0",
    "node": ">=18.11.0"
  }
}

Any idea, what I am doing wrong?

JSONSchema type does not support array as default

Defining an array as default value results in a ts-error

import { JSONSchema } from 'json-schema-to-ts';

// Works fine
export const validSchema: JSONSchema = {
  type: 'object',
  properties: {
    name: {
      type: 'string',
      default: 'stan',
    },
  },
  required: ['name'],
  additionalProperties: false,
} as const;

// Results in a ts-error
export const invalidSchema: JSONSchema = {
  type: 'object',
  properties: {
    names: {
      type: 'array',
      items: { type: 'string' },
      default: ['thomas', 'stan'],
    },
  },
  required: ['names'],
  additionalProperties: false,
} as const;

Here is the resulting ts-error:

const invalidSchema: JSONSchema7
Type '{ readonly type: "object"; readonly properties: { readonly names: { readonly type: "array"; readonly items: { readonly type: "string"; }; readonly default: readonly ["thomas", "stan"]; }; }; readonly required: readonly [...]; readonly additionalProperties: false; }' is not assignable to type 'JSONSchema7'.
Types of property 'properties' are incompatible.
Type '{ readonly names: { readonly type: "array"; readonly items: { readonly type: "string"; }; readonly default: readonly ["thomas", "stan"]; }; }' is not assignable to type 'Record<string, JSONSchema7> | { readonly [x: string]: boolean | { readonly $id?: string | undefined; readonly $ref?: string | undefined; readonly $schema?: string | undefined; ... 44 more ...; readonly examples?: readonly unknown[] | undefined; }; } | undefined'.
Types of property 'names' are incompatible.
Type '{ readonly type: "array"; readonly items: { readonly type: "string"; }; readonly default: readonly ["thomas", "stan"]; }' is not assignable to type 'JSONSchema7 | { readonly $id?: string | undefined; readonly $ref?: string | undefined; readonly $schema?: string | undefined; readonly $comment?: string | undefined; ... 43 more ...; readonly examples?: readonly unknown[] | undefined; } | undefined'.
Types of property 'default' are incompatible.
Type 'readonly ["thomas", "stan"]' is not assignable to type 'JSONSchema7Type | { readonly [x: string]: string | number | boolean | ... | { readonly [x: number]: string | number | boolean | ... | ... | null; readonly length: number; readonly toString: {}; ... 32 more ...; readonly [Symbol.unscopables]: {}; } | null; } | { ...; } | undefined'.
Type 'readonly ["thomas", "stan"]' is not assignable to type '{ readonly [x: string]: string | number | boolean | ... | { readonly [x: number]: string | number | boolean | ... | ... | null; readonly length: number; readonly toString: {}; readonly toLocaleString: {}; readonly pop: {}; ... 30 more ...; readonly [Symbol.unscopables]: {}; } | null; }'.
Index signature for type 'string' is missing in type 'readonly ["thomas", "stan"]'.ts(2322)

Issue when importing generated type into another module

Generated types are not recognized when imported in other modules.

export const CarSchema = {
    type: 'object',
    properties: {
        color: {
            type: 'string',
        },
    },
    required: ['color'],
    additionalProperties: false,
} as const;

export type Car = FromSchema<typeof CarSchema>; will result in type Car = unknown in the imported module. If I specify a deserialize option, it transforms into any: type Car = any and the error Type instantiation is excessively deep and possibly infinite appears.
I am aware of this FAQ, but since this CarSchema only has one string attribute, there should not be heavy computations ongoing. Is there maybe something I can do to resolve this issue?

Deno compatibility

Hi! Is it possible to use this library from Deno? I tried this:

import { FromSchema } from "https://cdn.skypack.dev/json-schema-to-ts";
// or
// import { FromSchema } from "https:/jspm.dev/json-schema-to-ts";

const fooSchema = {
  const: "foo",
} as const;

type Foo = FromSchema<typeof fooSchema>;

but got:

β–Ί  deno run json-schema-to-ts.ts
Check file:///Users/alexey.alekhin/github/tapad/github-actions-ci-dev/json-schema-to-ts.ts
error: TS2614 [ERROR]: Module '"https://cdn.skypack.dev/json-schema-to-ts"' has no exported member 'FromSchema'. Did you mean to use 'import FromSchema from "https://cdn.skypack.dev/json-schema-to-ts"' instead?
import { FromSchema } from "https://cdn.skypack.dev/json-schema-to-ts";
         ~~~~~~~~~~

or with jspm:

β–Ί  deno run json-schema-to-ts.ts
error: An unsupported media type was attempted to be imported as a module.
  Specifier: https://jspm.dev/npm:json-schema-to-ts/~
  MediaType: Unknown

I wonder if adding exports field in the package.json could help those CDNs to process it correctly?


Also, I got this message from skypack:

[Package Error] "[email protected]" could not be built.

[1/5] Verifying package is valid…
[2/5] Installing dependencies from npm…
[3/5] Building package using esinstall…
Running esinstall...
No ESM dependencies found!
At least one dependency must have an ESM "module" entrypoint. You can find modern, web-ready packages at https://www.skypack.dev
No ESM dependencies found!
At least one dependency must have an ESM "module" entrypoint. You can find modern, web-ready packages at https://www.skypack.dev

How to fix:

Schema Schema

Is there a Schema for the Schema?

const fooSchema : JsonSchema = {
  type: "object",
  properties: {
    bar: { type: "number" },
    type: { enum: ["one", "two", "tree"] },
  },
  required: ["bar", "type"],
} as const;

Support for "uri" type?

The following code gives me an error in vscode intellisense:

const inputSchema = {
  type: 'object',
  properties: {
    target: {
      type: 'string',
      minLength: 1,
    },
    url: {
      type: 'uri'
    },
    count: {
      type: 'integer',
      minimum: 0,
    },
    cookie: {
      type: 'string',
    },
  },
  required: ['url', 'count']
} as const

type Task = FromSchema<typeof inputSchema>

The error I get is: Types of property 'type' are incompatible. Type '"uri"' is not assignable to type 'JSONSchema6TypeName | readonly JSONSchema6TypeName[] | undefined'.ts(2344)

But when I change the type for "url" to "string", the problem goes away. Is the "uri" type not supported?

allOf doesn't work

Not quite sure how to reproduce this in a useful way, I have a schema like this, which extends a schema in allOf. Even though I have ensure that the accept exists in header in programmatic level, it is not reflected in the type generated by this library.

import { FromSchema } from "json-schema-to-ts";

const defHeaderSchema = {
	type: "object",
	properties: {
		header: {
			properties: {
				accept: {
					type: "string",
					pattern: "application/(?:\\*|json)|\\*/\\*"
				}
			},
			required: ["accept"]
		}
	},
} as const

const postSchema = {
	allOf: [ defHeaderSchema ],
	type: "object",
	properties: {
		body: {
			type: "object",
			properties: {
				number: {
					type: "string"
				},
			},
			required: ["number"]
		}
	}
} as const;

let obj!: FromSchema<typeof postSchema>

See the final type by hovering on obj

Enum doesn't work in TS 4.3.5

I'm not sure if it's just 4.3.5 or something earlier in the 4.x line but enums don't seem to work any more

const mySchema = {
  type: 'string',
  enum: ['one', 'two', 'three'] 
} as const

const MyType = FromSchema<typeof mySchema>
// = `never`

Does't work on deno?

deno: 1.15.2

import Ajv from "https://esm.sh/[email protected]";

const schema = {
  type: "object",
  "properties": {
    "foo": { "type": "string" },
    "bar": { "type": "number", "maximum": 3 },
  },
  required: ["foo", "bar"],
} as const;
const schemaString = JSON.stringify(schema);
console.info(schemaString);

const validate = new Ajv().compile(schema);

const test = (obj: any) => {
  if (validate(obj)) console.log("Valid:", obj);
  else console.log("invalid!:", obj, validate.errors);
};

test({ "foo": "abc", "bar": 2 });
test({ "foo": "abc", "bar": 9 });
test({ "foo": "ab", "bar": "2" });

// https://github.com/ThomasAribart/json-schema-to-ts
import { FromSchema } from "https://deno.land/x/[email protected]/index.d.ts";
type FooBar = FromSchema<typeof schema>;
const forbar: FooBar = { "foo": "abc", "bar": "x" };
console.log(forbar);

The code running output as follow, but const forbar: FooBar = { "foo": "abc", "bar": "x" }; should not compile success?

{"type":"object","properties":{"foo":{"type":"string"},"bar":{"type":"number","maximum":3}},"required":["foo","bar"]}
Valid: { foo: "abc", bar: 2 }
invalid!: { foo: "abc", bar: 9 } [
  {
    instancePath: "/bar",
    schemaPath: "#/properties/bar/maximum",
    keyword: "maximum",
    params: { comparison: "<=", limit: 3 },
    message: "must be <= 3"
  }
]
invalid!: { foo: "ab", bar: "2" } [
  {
    instancePath: "/bar",
    schemaPath: "#/properties/bar/type",
    keyword: "type",
    params: { type: "number" },
    message: "must be number"
  }
]
{ foo: "abc", bar: "x" }

How to best surface schema descriptions as type documentation?

As traditional types can have docblocks those can get surfaced in IDEs. How can I best surface descriptions to be available on these types?

import type { FromSchema } from 'json-schema-to-ts';

const dogSchema = {
  type: 'object',
  properties: {
    name: { type: 'string', description: "the dogs' name" },
    age: { type: 'integer' },
  },
  required: ['name', 'age'],
} as const;

type Dog = FromSchema<typeof dogSchema>;

// type Dog = {
//   /**
//    * the dogs' name
//    */
//   name: string;
//   age: number;
// };

function addDog(dog: Dog) {
  return dog;
}

addDog({ name: 'buster', age: 18 });

Traditional type on-hover:

Screen Shot 2022-10-14 at 11 43 33 AM

json-schema-to-ts on-hover:

Screen Shot 2022-10-14 at 11 43 21 AM

Provide Repository Changelog

Hi,

thanks for your work!
I noticed that the repository has just been updated to its latest version, moving it from v2.5.2 to v5.2.3.

Would it be possible to provide a CHANGELOG.md file, similar to the one proposed in keepachangelog website to briefly explain the changes that are introduced with each new tag?

Moreover, would it be possibile to describe the reason why repository version moved of three major versions? Is it possible that is just a typo?

Probably all versions above `1.6.4` are not working with `preserveSymlinks` tsconfig option

Hey, this is a cool library I really like the idea of inferring TS types from JSON schemas πŸ‘ŒπŸ»

Almost all the solutions I found are focused on generating types (via CLIs or code), this library is different & unique.

I found something interesting, all the recent versions (above 1.6.4) of this library are not working for me, I'm pretty sure my config is correct I'm using Typescript v4 and strict mode is enabled.

This is what I receive, the inferring is not working πŸ˜• Used the example in the Readme.

The interesting part is this, it looks like 1.6.4 is the most used version in the last week, so I test it and it's working perfectly!

Can anyone please confirm that versions above 1.6.4 work? or at least the recent one 2.5.5? Maybe I have something wrong on my side, thanks.

This is my config:

  • node v16.16.0
  • Typescript v4.7.4

tsc compiled js still imports json-schema-to-ts which imports as a "ts" file

I notice every other library in my node_modules has "main": "index.js" or similar, and import correctly in my project here.

Maybe you could have that file be a no-op in your published packages, and have "main": "index.js" pointing to that no-op, and "typings": "./lib/index.d.ts" instead, so that imports of 'json-schema-to-ts' don't start throwing errors like:

import { JSONSchema6Definition } from "json-schema";
^^^^^^

SyntaxError: Cannot use import statement outside a module
    at wrapSafe (internal/modules/cjs/loader.js:979:16)
    at Module._compile (internal/modules/cjs/loader.js:1027:27)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
    at Module.load (internal/modules/cjs/loader.js:928:32)
    at Function.Module._load (internal/modules/cjs/loader.js:769:14)
    at Module.require (internal/modules/cjs/loader.js:952:19)
    at require (internal/modules/cjs/helpers.js:88:18)
    at Object.<anonymous> (./dist/src/myfile.js:21:29)
    at Module._compile (internal/modules/cjs/loader.js:1063:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)

There may be a better way here, I'm not the leading expert on JS module resolution.

Possibility to make `as const` unnecessary

Maybe there is a way to remove the necessity of as const. Please view the following example:

type Narrow<T> =
  | T extends Function ? T : never
  | T extends string | number | boolean | bigint ? T : never 
  | T extends [] ? [] : never 
  | { [K in keyof T]: Narrow<T[K]> }

declare function test<T>(a: Narrow<T>): T


const asConst = test(['string', 'literal', 123])
// of type ['string', 'literal', 123]

Play with it in TS Playground

Source: https://twitter.com/hd_nvim/status/1578567206190780417

JSON schema "format" property

JSONSchema supports format on type = string and format date-time I want FromSchema to return Date object instead of string.

There may be other custom formats like object-id (MongoDB special type), so what I suggest is to allow users to specify mapping of format value to some scalar type:

type User = FromSchema<typeof schema, {
  format: {
    "date-time": Date,
    "object-id": ObjectId
  }
}>

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.