Specifications and references
What the generators accept as input, how a specification split across several files is read, how references to remote documents are redirected to local copies with refMappings, and which warnings and errors the loading step can produce. Read it when your specification is more than one file, or when a run stops with a reference error.
Everything on this page happens while the specification is loaded, before any generator-specific work, so it applies to all four generators in the same way.
Input formats
input names one file, in YAML or JSON. The format is detected from the content, not the extension: the file is parsed as YAML first and, if that fails, as JSON. The document must be an OpenAPI 3.0 or 3.1 specification, which is checked by the presence of its openapi field (a document with a swagger field is accepted by the loader as well). reference/openapi-feature-support.md lists which constructs each generator turns into output.
Problems with the input file stop that entry of the batch with a message naming the file:
| Problem | Message |
|---|---|
| File does not exist | Input file not found: specs/openapi.yaml |
| Neither YAML nor JSON parses | Failed to parse OpenAPI specification from: specs/openapi.yaml, followed by the parser's error |
| Empty, blank, or not an object (a bare list or string) | Empty or invalid OpenAPI specification in specs/openapi.yaml |
No openapi or swagger field | Invalid OpenAPI specification: missing 'openapi' or 'swagger' field in specs/openapi.yaml |
An empty file is a common accident (a copy step that produced a zero-byte file, a template that was never filled in). It is reported like any other invalid input, the entry fails, the other entries in the batch still run, and the command exits with status 1:
Processing [1/1] openapi.yaml...
✗ Failed to generate src/schemas.ts: Empty or invalid OpenAPI specification in openapi.yaml
The zod generator additionally needs components.schemas to exist; a specification without it fails with No schemas found in OpenAPI spec.
References across files
A $ref may point into another document. This specification, openapi.yaml, keeps its schemas in two further files:
openapi: 3.0.3
info:
title: Orders
version: 1.0.0
paths:
/orders:
get:
operationId: listOrders
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
$ref: "./common.yaml#/components/schemas/Order"
components:
schemas:
Money:
type: object
properties:
localOnly:
type: boolean
required:
- localOnly
common.yaml, next to it, declares Order and its own Money:
components:
schemas:
Order:
type: object
properties:
id:
type: string
total:
$ref: "./money.yaml#/components/schemas/Amount"
required:
- id
Money:
type: object
properties:
currency:
type: string
amount:
type: number
money.yaml, also next to it, declares Amount:
components:
schemas:
Amount:
type: object
properties:
value:
type: number
currency:
type: string
When openapi.yaml is loaded, every external reference is resolved: the referenced file is read, the target is copied into the root specification's own components, and the $ref is rewritten to point at the copy. References are followed transitively (Order brings Amount with it), a cycle between two documents terminates, and every component type is supported, not only schemas. A Swagger 2 style #/definitions/<name> pointer is accepted alongside #/components/<type>/<name>. The generators then see a single, self-contained document, and the generated file is self-contained too:
export type Amount = {
value?: number;
currency?: string;
};
export type Order = {
id: string;
total?: Amount;
};
/**
* Response for listOrders (200)
*/
export type ListOrdersResponse = Order[];
Two rules decide where a referenced file is looked for:
- A relative path resolves against the document that contains the
$ref, not against your working directory. The reference incommon.yamlto./money.yamlreads themoney.yamlnext tocommon.yaml, wherever you ran the generator from. Absolute paths are used as they are. - The files must exist when you generate. A build that copies only the entry-point specification fails with
Referenced file not found: <absolute path>, followed byReferenced from: <the document that referenced it>.
Two situations are reported as warnings and the reference is left as it was: a pointer that does not name a component (Reference "./common.yaml#/info" does not point at a component and cannot be inlined; only #/components/<type>/<name> and #/definitions/<name> are supported.) and a pointer whose target does not exist in the other file (Reference "./common.yaml#/components/schemas/Missing" does not resolve in that document.). A $ref inside a single document that names a component which does not exist is also a warning, because the construct that declared it will be missing from the output: Reference "#/components/schemas/Missing" could not be resolved; the construct that declared it will be missing from the generated output.
When an imported name clashes with yours
common.yaml above declares a Money schema, and so does openapi.yaml. Yours wins. The imported one is inlined under a name qualified with the file it came from, and the run says so:
[@spartaficial/openapi-to-typescript] Warnings:
⚠️ "Money" from ./common.yaml collides with a name already in this spec; it was inlined as "CommonMoney".
export type Money = {
localOnly: boolean;
};
export type CommonMoney = {
currency?: string;
amount?: number;
};
Nothing is silently overwritten, and every reference to the imported schema points at CommonMoney.
Remote references and refMappings
Specifications exported from a shared schema registry often reference it by absolute URL:
components:
schemas:
Price:
$ref: "https://schemas.acme.com/v2/money.yaml#/components/schemas/Amount"
Overridden:
$ref: "https://schemas.acme.com/v2/special/money.yaml#/components/schemas/Amount"
Remote documents are never fetched. Downloading a file during generation would put a network call, and whatever happens to be served at that URL that day, inside your build. A remote reference with no mapping therefore fails the entry, naming the URL and showing the shape of a mapping that would fix it:
✗ Failed to generate src/types.ts: Remote reference "https://schemas.acme.com/v2/money.yaml" cannot be resolved.
Remote documents are never fetched during generation. Map the URL to a local file:
refMappings: {
"https://schemas.acme.com/v2/": "./path/to/local/dir/"
}
Paths are relative to the working directory.
refMappings redirects URL prefixes to local directories. Commit the documents you depend on next to your specification and point the prefix at them:
export default defineConfig({
specs: [
{
input: "openapi.yaml",
outputTypes: "src/types.ts",
refMappings: {
"https://schemas.acme.com/v2/": "./specs/acme/",
"https://schemas.acme.com/v2/special/": "./specs/legacy/",
},
},
],
});
With this configuration Price reads ./specs/acme/money.yaml and Overridden reads ./specs/legacy/money.yaml; generation stays offline and reproducible. The rules:
- A mapping applies when the reference's URL starts with the key, and the rest of the URL is appended to the value. A complete URL as the key therefore works as an exact match for one document.
- When several keys match, the longest wins. The example redirects the whole
v2registry to./specs/acme/and sends the documents underv2/special/somewhere else, with no ordering to remember. - Values are relative to the working directory, the same as
inputandoutputTypes, because they come from your configuration rather than from a specification. This differs from relative$refs inside a document, which resolve against that document. Absolute paths work too. - Only
httpandhttpsreferences consult the mappings. Relative references already know where to look. - Once a remote document has been read, references inside it resolve against that local file, so a registry document that references its neighbours by relative path works as it did in the registry.
refMappings can live in defaults, so one set of mappings covers every entry in specs. The option is listed in each generator's reference page, for example reference/options-zod.md.
Name collisions
The generators turn schema names and operation names into identifiers, and that normalisation collapses separators and case: pet-kind and Pet_Kind both become PetKind (see naming.md). When two entries in a specification normalise to the same identifier, the generated file would declare it twice. Generation still completes and a warning names both sources:
Name collision: "schema "Pet_Kind"" and "schema "pet-kind"" both generate "PetKind". The generated file will declare it twice - rename one, or use prefix/suffix to separate them.
Nothing is renamed automatically, because a name that depended on declaration order would change under you. In the TypeScript output a duplicate declaration fails to compile, which is loud. In the zod output the duplicate is a const, and the second declaration shadows the first for every importer, so the wrong schema would validate without any error; the warning is the only indication, which is why it is worth reading. The same check covers method names in the Playwright and k6 clients, where two operations can produce one method name.
Warnings and showWarnings
Warnings are collected during a run and printed together after the entry they belong to, prefixed with the generator's name:
[@spartaficial/openapi-to-k6] Warnings:
⚠️ All 15 operations were filtered out. Check your operationFilters configuration.
⚠️ No output generated - no operations found after filtering
A warning never changes the outcome: the entry counts as successful and the exit status stays 0. The loading step reports an imported name that was renamed, a reference that does not resolve and a webhook-only document; the generators add collisions, filters and ignoreHeaders patterns that matched nothing, allOf members whose properties conflict, and other constructs they recognise but cannot fully honour. Set showWarnings: false on an entry or in defaults to silence all of them; the output deprecation warning is the one message that is printed regardless, because it concerns the configuration rather than the specification. Treat a silenced run as a decision you have taken, not as a clean one.