npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

genever

v0.7.1

Published

Generate TypeScript types from JSON Schema files and OpenAPI documents, with strict schema checks and the same types Juniper infers.

Readme

genever

Generate TypeScript types from JSON Schema files and OpenAPI documents, with strict schema checks and the same types Juniper infers.

npm package License

  • Types that match the schema — tuples (prefixItems), oneOf unions you can narrow, patternProperties keys like `x-${string}`, recursive $defs and if/then/else all become precise types, not any or unknown[].
  • Round-trips Juniper — a schema built with Juniper, serialized and read back by genever, gives the same type Juniper inferred. Every release is tested against that.
  • Strict by default — unknown keywords, keywords that don't apply to the schema's type, undeclared required properties, and contradictions like minLength over maxLength fail generation, instead of silently changing what the schema accepts. Without strict mode, the type follows exactly what a validator accepts.
  • One file from many — $refs to other files and URLs are loaded and generated into the same output, with $id and $anchor resolved as the spec says.
  • Generated files stay in sync — a --ci mode fails when a committed file is out of date, like populate-files.
  • Types only — the output is plain TypeScript types, with no runtime dependency.

Guides and the full reference are on the docs site. For the motivation, and how it compares to other generators, see WHY-GENEVER.md.

Contents

Install

npm i -D genever

ESM only, and requires Node 24.22.1 or later (it fetches remote schemas, and earlier releases have security flaws in fetch). It is a dev dependency: the output is plain TypeScript with no runtime import of genever.

Example

user.json:

{
  "type": "object",
  "properties": {
    "id": { "type": "string", "pattern": "^usr_" },
    "home": { "$ref": "#/$defs/Address" },
    "point": {
      "type": "array",
      "prefixItems": [{ "type": "number" }, { "type": "number" }],
      "items": false,
      "minItems": 2
    }
  },
  "required": ["id"],
  "$defs": {
    "Address": {
      "type": "object",
      "properties": { "city": { "type": "string" } },
      "required": ["city"]
    }
  }
}
npx genever user.json --name User

user.ts:

// Generated by genever. Do not edit by hand, regenerate it instead.

export type User = {
    /**
     * @pattern "^usr_"
     */
    id: `usr_${string}`;
    home?: Address;
    /**
     * @minItems 2
     */
    point?: [number, number];
};

export type Address = {
    city: string;
};

Usage

genever is an ESM module, and a CLI. Most projects run the CLI as a build step, and commit the generated file. In CI, --ci (on by default when the CI environment variable is set) fails instead of writing when the file is out of date.

The programmatic API is the same: genever(schema) returns the TypeScript source, and geneverFile(path) reads a schema file and writes the types next to it.

The root schema becomes a type named Schema (or --name). Every definition in the root $defs (or draft 7 definitions) becomes an exported type named after its key, in PascalCase. A schema referenced by $ref from elsewhere in the document becomes a named type too, named after its key in any nested $defs, or else after its path. Everything else is inlined.

References to other files

A $ref may point at another file, or a URL, relative to the schema it is in: { "$ref": "./address.json" }, { "$ref": "https://example.com/schemas/user.json#/$defs/Name" }. The CLI and geneverFile load each one, and everything it references in turn, and the output is still one file:

  • In an OpenAPI document, any schema may: a component, or a schema in an operation's parameters, request body or responses.
  • An OpenAPI document may be split across files. A reference object (a path item, parameter, request body, response or header that is only a $ref) may point at another file, such as ./paths/pets.yaml or ./responses.yaml#/NotFound, and a schema may point into another OpenAPI document, such as ./common.yaml#/components/schemas/Error. Of another document, only the schemas used become types, named as in the input (Error). Its schemas keep its own OpenAPI version, and a file of OpenAPI objects without one has the version of the document that refers to it.
  • Each referenced file becomes a type named after its title, or else its file name (address.json is Address). Two of the same name are numbered (Address2).
  • A file referenced twice is one type.
  • A file that only holds $defs (shared definitions) is not a type itself, but its definitions are, when referenced.
  • Files without a $schema are read in the input's dialect: JSON Schema 2020-12, or the OpenAPI version of an OpenAPI document.
  • A file or URL that can't be loaded throws a GeneverLoadError.

Loaded files are embedded with an absolute $id, following the bundling process of JSON Schema 2020-12, so $refs resolve to them unchanged, and $id and $anchor inside them work as in the original files. A relative $ref in a URL resolves against where it redirected to, if it did. --no-fetch (or fetch: false) loads files only, never URLs: a $ref to a URL fails with a GeneverLoadError, unless a loaded schema declares that URL as its $id, such as a copy committed next to the input. --fetch-host example.com (or fetch: ['example.com']) loads only URLs of the hosts listed, and only follows redirects to them. A host matches on any port unless it names one (localhost:8080), and *.example.com matches subdomains of example.com, but not example.com itself. genever() itself reads one document, and doesn't load anything: bundle first, or use geneverFile.

Checking without fetching

--ci regenerates the file, so it fetches every URL again, and fails when a remote schema changed even though nothing in the repository did. With --manifest generated.json (or manifest), genever also records the output in a populate-files manifest, with hashes of everything it read, its own version, and the options. --ci with the same manifest then passes without loading anything if all of these hold: the version and options match, the output is unedited, and every file it read is unchanged. URLs are trusted as recorded. Otherwise it regenerates as usual, and fails with what is out of date. Run genever without --ci to pick up remote changes, which then show up as a diff in review.

npx genever schema.json --manifest generated.json         # writes schema.ts and generated.json
npx genever schema.json --manifest generated.json --ci    # in CI: no network needed

OpenAPI documents

An OpenAPI 3.0 or 3.1 document (a JSON or YAML file with an openapi version) is read too. Each of its components.schemas becomes an exported type named after its key, and there is no root type. The version decides the dialect: 3.0 schemas may use nullable, and 3.1 schemas are JSON Schema 2020-12. Either may use OpenAPI's own keywords (discriminator, xml, externalDocs, example), which don't change the type.

npx genever openapi.yaml -o src/api-types.ts

A document with paths also gets Paths (by path, then method) and Operations (by operationId) types, in the shape openapi-typescript generates: each operation's parameters by location, requestBody, and responses by status code with headers and content by media type.

type User = Paths['/users/{id}']['get']['responses'][200]['content']['application/json'];
type NewUser = Operations['createUser']['requestBody']['content']['application/json'];

Two generic types, keyed by operationId, put an operation's types in the shape a typed client or server passes around. A misspelled operationId is a type error:

// { readonly body: NewUser } & { readonly path: { orgId: string } }: each part only if the operation has it
type Create = OperationInput<'createUser'>;
// { readonly status: 201; readonly body: User } | { readonly status: 400; readonly body: ApiError }
type Created = OperationResult<'createUser'>;

On a server, a validator that fills in defaults (Ajv's useDefaults) leaves a parameter whose schema has a default always there, though OperationInput has it optional, for callers. With --parameter-defaults, genever also writes OperationRequest<Id>, the handler's view: as OperationInput, but with those parameters required. Request bodies are left as sent.

YAML

A .yaml or .yml file is read as YAML 1.2, whether it is the input or a file it references, and so is a URL served with a YAML media type. Anchors and aliases are expanded, and merge keys (<<: *base) merged. An alias inside the schema it names, a recursive schema, becomes a $ref to that schema, so it is a recursive type. YAML 1.2 reads on, off, yes and no as strings, not booleans. Anything else JSON can't hold is an error: tags beyond YAML's core schema (such as !!binary), keys that aren't strings, duplicate keys and more than one document throw the yaml package's YAMLError (invalid JSON throws a SyntaxError), and .inf, .nan and an alias inside a value that isn't a schema (such as a const) throw a GeneverInvalidSchemaError. In a referenced file, any of these is a GeneverLoadError.

The generated file is formatted with format-file, using your project's Biome or Prettier config if either is installed.

Precise types

Where other generators commonly lose type information, genever keeps it:

| Schema | genever | Often generated instead | |---|---|---| | prefixItems: [{ number }, { number }], items: false | [number, number] | never[], any[], or [number, number, ...number[]] | | oneOf of objects with a kind const | a union that narrows on kind | one object, with each branch's properties optional, or any | | pattern: "^usr_" | `usr_${string}` | string | | patternProperties: { "^x-": string }, additionalProperties: false | { [key: `x-${string}`]: string } | { [k: string]: unknown }, or string for every key | | recursive $defs | recursive named types | any after a few levels, or undefined | | enum: ["a", 42, null] | "a" \| 42 \| null | string \| number \| null |

Each row is one of the probes in WHY-GENEVER.md, which compares genever with five other tools on the same schema.

How schemas become types

| JSON Schema | TypeScript | |---|---| | true, {} | unknown | | false, { "not": {} } | never | | type | string, number (also integer), boolean, null; a type array is a union | | pattern of a literal prefix, suffix or substring (^abc, abc$, abc) | `abc${string}`, `${string}abc`, `${string}abc${string}` | | any other pattern, format, lengths, ranges | the plain type, as TypeScript can't express them | | enum, const | literal types, including objects and arrays | | items | T[] | | items + minItems up to 3 | [T, ...T[]], with minItems leading items. Larger values stay T[] (see typedMinItems) | | prefixItems + items: false | [A, B], with items beyond minItems optional | | prefixItems + items | [A, B, ...T[]] | | draft 7 items array + additionalItems | same as prefixItems + items | | properties + required | { a: A; b?: B } | | additionalProperties: true / schema | [key: string]: unknown / T | | patternProperties | an index signature, keyed by `x-${string}` for a literal prefix like ^x-, or string otherwise | | type: "object" with no properties | Record<string, unknown> | | dependentRequired, dependentSchemas, draft 7 dependencies | { a?: never } \| { b: B } | | allOf | intersection | | anyOf, oneOf | union | | if / then / else | (If & Then) \| Else | | not of null or literal values | removes them from a union; any other not leaves the type | | $ref | the referenced named type. It resolves against the nearest $id, so #/$defs/A inside an embedded resource is that resource's A, and may name an $anchor | | draft-04 id, and boolean exclusiveMinimum/exclusiveMaximum | in a schema whose $schema is draft-04, or inside one: id names a resource as $id does, and the booleans make minimum and maximum exclusive. Elsewhere, id is an unknown keyword | | propertyNames with a literal prefix or enum | the keys of additionalProperties: `x-${string}`, or optional properties a, b | | title, description, deprecated | JSDoc | | default, examples, format, and constraints such as minLength or maximum | JSDoc tags, e.g. @minLength 3, so editors show them |

Subschemas of allOf, anyOf, oneOf, if/then/else and not without a type of their own apply to their parent's types, as Juniper writes them. So { "type": ["string", "null"], "allOf": [{ "pattern": "^a" }] } is `a${string}` | null.

Without additionalProperties, an object type has no index signature: TypeScript objects already allow other keys, and reading an undeclared key stays an error.

Not supported

  • $dynamicRef and $recursiveRef throw GeneverUnsupportedError. genever() only reads one document, so with it, a $ref to another file does too (see References to other files). References may use $id and $anchor.
  • unevaluatedProperties, unevaluatedItems and contains are accepted, but don't change the type.
  • discriminator is accepted in plain JSON Schema too, since Juniper and Ajv use it there, and doesn't change the type: the branches narrow on the property as a union does. Strict mode checks it can tell the branches apart: the property must be required, by the schema or by every oneOf/anyOf branch, and every mapping target must exist. Its values aren't checked, as OpenAPI may take them from component names.
  • Swagger 2.0 documents.
  • An OpenAPI document's webhooks and operations' callbacks have no types, though their schemas in components do.

Differences from Juniper

genever is tested to generate the same type Juniper infers, for Juniper schemas covering each of its schema types and keywords. The exceptions are where the JSON says more than Juniper's type does, or less:

  • An object schema with no properties is Record<string, unknown>, not Juniper's empty object type.
  • Prefix items beyond minItems are optional. Juniper's tupleSchema() always sets minItems, so its tuples are identical.
  • In if/then/else, a property set to false in else is ?: never.
  • A type from .cast<T>() or customSchema<T>() exists only in TypeScript, so genever reads the JSON it emits instead.

Strict mode

Strict mode (the default) fails with a GeneverStrictModeError on schemas that are valid JSON Schema, but most likely mistakes. Validators ignore these, so the schema silently accepts more, or less, than it appears to.

Keywords that do nothing:

  • An unknown keyword. Keywords starting with x- are allowed, as are OpenAPI's keywords in an OpenAPI document or with openApi30.
  • A type specific keyword (e.g. maxLength) in a schema without a type, or whose type it doesn't apply to (e.g. maxLength on a number). Subschemas inherit their parent's type.
  • A required property that isn't in properties, of the schema or a parent.
  • then or else without if.

Schemas that accept nothing, or less than they declare:

  • minLength greater than maxLength, and the same for minItems/maxItems, minProperties/maxProperties and minContains/maxContains.
  • minimum/exclusiveMinimum and maximum/exclusiveMaximum that no number (or no integer, for integer) fits between.
  • An enum or const value that isn't of the schema's type.
  • A required property whose schema is never valid (e.g. false), or more required properties than maxProperties allows.
  • minProperties more than an object with additionalProperties: false can have.
  • A tuple (prefixItems with items: false) shorter than minItems, or longer than maxItems.
  • A $ref back to a schema it is part of, without a property or item in between, such as { "anyOf": [{ "$ref": "#" }, { "type": "string" }] }. It applies to the same value again, so a validator never finishes checking it, and JSON Schema leaves its behaviour undefined.

Every schema Juniper builds passes.

Without strict mode

Disable strict mode (strict: false, --no-strict) for schemas you don't control. The generated type then follows what a validator accepts, and is never narrower:

  • Unknown keywords, and keywords that don't apply to the type, are ignored.
  • A schema without a type accepts every type. { "properties": { "a": … } } is the object type, or any string, number, boolean, null or array.
  • A type whose constraints contradict each other is never. For { "type": ["string", "null"], "minLength": 3, "maxLength": 2 }, only null is valid.
  • Tuple items beyond maxItems are left out.
  • A $ref back to a schema it is part of, without a property or item in between, is never: it can't be why a value is valid. The anyOf above is string. A schema it passes through on the way back is inlined, as TypeScript rejects a type that refers to itself outside an object or array.

Stability of generated files

The text of a generated file is not part of genever's API. Formatting, ordering, and how a type is written (e.g. Record<string, number> or { [key: string]: number }) may change in any release.

The types TypeScript reads from it are. A change that makes a generated type accept or reject different values, or read differently (such as a union becoming an intersection), is a breaking change.

API

genever(schema, options?)

Generate TypeScript types from a JSON Schema.

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | schema | unknown | — | Required. Parsed JSON Schema, or OpenAPI document. | | options.name | string | 'Schema' | Name of the root schema's type. Not used for an OpenAPI document. | | options.strict | boolean | true | Fail on likely mistakes. See Strict mode. | | options.baseUri | string | a placeholder | URI the schema was read from, which relative $ids and $refs resolve against. | | options.parameterDefaults | boolean | false | With an OpenAPI document, also generate OperationRequest<Id>: what a handler gets once a validator that fills in defaults has run. | | options.typedMinItems | number | 3 | Largest minItems typed as required items, e.g. [T, ...T[]]. 0 turns it off. The default matches Juniper. | | options.openApi30 | boolean | false | Read the schema as an OpenAPI 3.0 schema, which allows nullable and example. An OpenAPI document's own version is used instead. |

Returns string — TypeScript source, not formatted.

Throws GeneverError — see Errors.

import { genever } from 'genever';

genever({ type: 'array', items: { type: 'string' } }, { name: 'Tags' });
// export type Tags = string[];

geneverFile(input, options?)

Read a JSON Schema file, and write its types to a TypeScript file.

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | input | string | — | Required. Path to the JSON Schema file. | | options.output | string | input path with .ts instead of .json | Path of the TypeScript file to write. | | options.cwd | string \| URL | process.cwd() | Directory relative paths resolve from. | | options.check | boolean | false | Fail, rather than write, if the output file is not up to date. | | options.dryRun | boolean | false | Do not write the output file. | | options.signed | boolean | false | Sign the output file with generated-signature, so edits to it are detected. An output file that was edited, or is not signed yet, is not overwritten (a PopulateFilesOwnershipError from populate-files). | | options.force | boolean | false | With signed, overwrite the output file anyway. Use it once to start signing an existing file. | | options.manifest | string | — | Path of a manifest (relative to cwd) to record the output and hashes of its inputs in. With check, an output matching its entry is up to date without loading anything. See Checking without fetching. | | options.fetch | boolean \| string[] | true | Load http: and https: URLs the schema references, or only those of the hosts listed. Otherwise only files are loaded. See References to other files. | | options.name, options.strict, options.openApi30, options.parameterDefaults, options.typedMinItems | | | Same as genever. |

Returns Promise<{ output: string; updated: boolean }> — absolute path of the output file, and whether its content changed.

Throws GeneverError — see Errors. With check, also throws a PopulateFilesCheckError (from populate-files, ... not up to date. Reason: content-changed) if the file is out of date. Rejects with the file system's error (ENOENT) if input doesn't exist, and a SyntaxError for invalid JSON. Throws a TypeError if fetch lists something that is not a host.


Errors

Every error extends GeneverError, with a pointer property: the JSON Pointer to the schema or keyword at fault, e.g. #/properties/a/type. In another OpenAPI document, or file of OpenAPI objects, it starts with that file's URI, e.g. file:///api/common.yaml#/components/schemas/Error/type. The pointer is also at the end of the message.

| Error | Thrown when | |-------|-------------| | GeneverInvalidSchemaError | The input is not a valid JSON Schema, e.g. properties is not an object, or a $ref does not resolve. | | GeneverStrictModeError | A strict mode rule is broken. | | GeneverUnsupportedError | The schema uses a feature genever can't generate types for. See Not supported. | | GeneverLoadError | A file or URL the schema references could not be loaded. Its pointer is to the $ref that needed it, in the file or URL its referencedFrom names: the input, or a file the input loaded. Its cause is the underlying error, such as the file system's or the parser's, if there was one. |

CLI

genever <input> [options]

| Option | Default | Description | |--------|---------|-------------| | -o, --output | input path with .ts | Path of the TypeScript file to write. | | --name | Schema | Name of the root schema's type. Not used for an OpenAPI document. | | --strict | true | Fail on likely mistakes. Disable with --no-strict. | | --parameter-defaults | false | With an OpenAPI document, also write OperationRequest, with parameters' defaults filled in. | | --typed-min-items | 3 | Largest minItems typed as required items. 0 turns it off. | | --open-api-30 | false | Read the schema as an OpenAPI 3.0 schema. An OpenAPI document's own version is used instead. | | --ci | true in CI | Fail if the output file is not up to date. Implies --dry-run. | | --dry-run | false | Do not write the output file. | | --signed | false | Sign the output file, so edits to it are detected, and refuse to overwrite it if it was edited or is not signed. | | --force | false | With --signed, overwrite the output file anyway. | | --manifest | | Record the output, and hashes of its inputs, in this manifest. With --ci, an output that still matches it passes without fetching URLs. | | --fetch | true | Load URLs the schema references. --no-fetch loads only files. | | --fetch-host | | Load only URLs of this host, e.g. example.com, localhost:8080, or *.example.com for its subdomains. Repeat for more hosts. | | --cwd | . | Directory relative paths resolve from. |

The path of a written file is logged. Nothing is logged when it was already up to date. Any error, including a failed --ci check, prints its stack trace and exits with code 1.

Why "genever"?

Juniper builds schemas with their types. Juniper berries are what give gin its flavor, and genever is gin's Dutch ancestor: genever takes what Juniper made, and gets its types back.

Compared to alternatives

  • json-schema-to-typescript — the most used generator. 2020-12 tuples become never[], pattern is always string, and patternProperties open the object to [k: string]: unknown.
  • quicktype — many languages, but merges oneOf objects into one type with optional fields, and widens enum numbers.
  • json-schema-to-zod — generates a validator, but every $ref becomes z.any().
  • json-schema-to-ts, TypeBox Static — infer types in TypeScript instead of generating them, so the schema has to be pasted into TypeScript as const. An imported .json file widens to unknown.

Tested side by side in WHY-GENEVER.md.

Also See

  • juniper — build JSON Schemas in TypeScript, with their types. genever reads them back.
  • populate-files — writes genever's output, and checks it in CI.