OpenAPI documents
Most schemas arrive inside an OpenAPI document. Pass the document itself:
npx genever openapi.yaml -o src/api-types.tsgenever reads any JSON or YAML file with an openapi version as a document:
- Every
components.schemasentry becomes an exported type, named after its key. There is no root type, so--nameis not used. - A
$refto#/components/schemas/Useris the typeUser. - The version decides the dialect: OpenAPI 3.0 schemas may use
nullable, and OpenAPI 3.1 schemas are JSON Schema 2020-12. - OpenAPI's own keywords,
discriminator,xml,externalDocsandexample, are allowed in strict mode, and don't change the type.exampleis written as a JSDoc@example.
{
"openapi": "3.0.3",
"info": { "title": "Users", "version": "1.0.0" },
"paths": {},
"components": {
"schemas": {
"User": {
"type": "object",
"properties": {
"name": { "type": "string", "nullable": true },
"home": { "$ref": "#/components/schemas/Address" }
},
"required": ["name"]
},
"Address": {
"type": "object",
"properties": { "city": { "type": "string" } }
}
}
}
}export type Address = {
city?: string;
};
export type User = {
name: string | null;
home?: Address;
};A .yaml or .yml document is read as YAML 1.2. See YAML.
A single OpenAPI 3.0 schema, outside a document, needs --open-api-30 to allow nullable.
Operations
A document with paths also gets two types for its operations, in the shape openapi-typescript generates:
Paths, keyed by path, then method. A method the path doesn't have is?: never.Operations, keyed byoperationId.Pathsrefers to these entries, and an operation without anoperationIdis only inPaths.
Each operation has its parameters by location (query, header, path and cookie), its requestBody, and its responses by status code, each with headers and content by media type. Look types up by key:
import type { Operations, Paths } from './api-types.js';
type User = Paths['/users/{id}']['get']['responses'][200]['content']['application/json'];
type NewUser = Operations['createUser']['requestBody']['content']['application/json'];
type UserQuery = Operations['getUser']['parameters']['query'];- A location without parameters is
?: never, and one whose parameters are all optional is optional. Path parameters are always required. An operation's parameters replace its path item's of the same name and location. requestBodyis optional unless the body isrequired, and?: neverwithout one. An optional one includesundefined, so look into it withNonNullable<Operations['updateUser']['requestBody']>['content'].- A response without
contenthascontent?: never.headershas every declared header, and allows any other. $refs tocomponents.parameters,requestBodies,responsesandheaders, and to path items, are followed. Schemas are the component types, likeUser.- Every media type is included, not only JSON. A media type without a schema is
unknown. summary,descriptionanddeprecatedbecome JSDoc on each operation, parameter, request body, response and header.
Keyed by operationId
Two generic types put an operation's types in the shape a typed client or server passes around, keyed by operationId, so a misspelled one is a type error:
OperationInput<Id>: what a call sends. Its parameters by location (path,query,header,cookie) and its JSONbody, each only if the operation has them, and optional when they are.OperationResult<Id>: every response the operation documents, as{ status, body }, with the JSON content asbody(undefinedwithout one). Checkingstatusnarrowsbody. A status that isn't a number (default,2XX) isnumber.
const created = await call('createUser', { body: { name: 'Dolly' } });
if (created.status === 201) {
created.body.id;
}On a server, a validator that fills in defaults (such as Ajv's useDefaults) leaves a parameter whose schema has a default always there, though OperationInput has it optional, for callers. With --parameter-defaults (parameterDefaults: true), genever also writes OperationRequest<Id>, the handler's view: as OperationInput, but with those parameters required, following a $ref to a schema's default too. Request bodies are left as sent: filling in their defaults is up to the validator, and isn't typed.
The OpenAPI demo builds its client and server on the same idea, and its server's handlers take OperationRequest.
Component schemas keep their names, so a component called Paths or Operations makes these Paths2 or Operations2, and likewise for OperationInput and OperationResult. Two operations with the same operationId fail with a GeneverInvalidSchemaError.
With Juniper
A spec built with Juniper's components() generates the same types Juniper inferred, for 3.0 and 3.1 alike. So a service can publish its spec, and a client generate matching types from it, with neither depending on the other's code.