Skip to content

02Guides

Overlays

Patch third-party specifications with OpenAPI Overlays instead of forking them.

Third-party specifications often need small fixes before they generate a good client: a missing operationId, a wrong nullable, a field typed string that is really an integer, an internal endpoint you do not want in your client. An OpenAPI Overlay describes those fixes in a separate file of ordered actions, so you do not fork the specification and the fixes keep applying when it is updated.

export default defineConfig({
spec: {
version: '3.1',
input: 'https://api.example.com/openapi.json',
overlays: ['./overlays/fixes.overlay.yaml'],
},
});

spec.overlays lists overlay documents (JSON or YAML; local paths relative to the config file, or http(s) URLs) that are applied in order. Each one is applied to the result of the previous one.

overlay: 1.1.0
info:
title: Fixes for the Example API
version: 1.0.0
actions:
- target: $.paths['/internal/metrics']
description: Not for clients.
remove: true
- target: $.paths['/pets'].get
update:
operationId: listPets
- target: $.components.schemas.Pet.properties.id
update:
type: integer
format: int64
- target: $.paths.*.*.parameters[?@.name == 'X-Debug']
remove: true
- target: $.paths['/pets'].get.tags
update: [pets]

Every action has a target, an RFC 9535 JSONPath query, and one of:

Field Effect on each selected node
update Merged into the node. Objects merge recursively: new properties are added, primitives replace primitives, arrays are concatenated, objects merge again; other combinations (an object into a string) are an error. An array node gets an array value concatenated, any other value appended. A primitive node is replaced.
copy (Overlay 1.1) A JSONPath query selecting exactly one node of the document being patched; its value is merged like update.
remove: true The node is removed from its object or array.

remove wins over update, and update over copy. A target that selects nothing changes nothing, and a node selected through several paths is changed once.

overlay: 1.2.0
info: { title: Internal operations, version: 1.0.0 }
components:
actions:
internal:
fields:
update:
x-internal: true
tags: [internal]
actions:
- $ref: '#/components/actions/internal'
target: $.paths['/admin/users'].get
- $ref: '#/components/actions/internal'
target: $.paths['/admin/audit'].get

The fields of the reference itself (target, description, or an update of its own) take precedence over the reusable action’s fields.

Targets are RFC 9535 queries: $.paths['/pets'] (member names with special characters need brackets), $..description (any depth), $.paths.*.get, $.tags[0], $.tags[-1], slices ([1:3]), and filters with comparisons, &&, ||, ! and the functions length(), count(), match(), search() and value():

$.paths.*[?@.deprecated == true]
$.components.schemas[?@.type == 'object' && !@.required]
$..parameters[?match(@.name, 'X-.*')]

The implementation passes the JSONPath Compliance Test Suite. Non-standard extensions that some tools accept (in, =~, script expressions) are syntax errors: they would not be portable. match() and search() take I-Regexp patterns; a pattern outside that grammar (lookarounds, backreferences, lazy quantifiers, \d) matches nothing, as RFC 9535 requires.

  • Overlay 1.0, 1.1 and 1.2 are supported. Every version is applied with the 1.1 rules, which clarified 1.0 (array concatenation, primitive targets). A newer 1.x version is applied with a warning; other major versions are rejected.
  • Every specification version: overlays patch the parsed document before swaggerz reads it, so they work for Swagger 2.0 and OpenAPI 3.0, 3.1 and 3.2 alike.
  • Overlays apply to the root document (spec.input). Documents it references with $ref are not patched; target the referencing node, or patch the referenced file’s $ref itself.
  • extends is ignored: the config decides which document an overlay applies to.
  • Plugins’ beforeParse hooks receive the patched document.

Problems point into the overlay file, with line and column: an invalid JSONPath (overlay/invalid-target), a target that matches nothing (overlay/no-match, a warning: the specification may have changed since the overlay was written), an incompatible update, a copy source that is not exactly one node. See diagnostics. With --verbose, overlay/applied says how many actions were applied.

Local overlay files are watched by generate --watch, the dashboard and the MCP server, like the specification. Remote overlays are fetched with the spec.fetch timeout and size limit (configured headers are sent only when the overlay is on the specification’s own origin) but not polled.

What's generated

openapi: 3.1.0
info:
title: Vendor API
version: '2024-06'
paths:
/pets:
get:
tags: [pets]
responses:
'200':
description: The pets.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
/internal/metrics:
get:
tags: [internal]
responses:
'204':
description: Metrics were flushed.
components:
schemas:
Pet:
type: object
properties:
id:
type: string
overlay: 1.1.0
info:
title: Fixes for the vendor API
version: 1.0.0
actions:
- target: $.paths['/pets'].get
update:
operationId: listPets
summary: List the pets.
- target: $.paths['/internal/metrics']
remove: true
- target: $.components.schemas.Pet.properties.id
update:
type: integer

pets/list-pets.ts

/* eslint-disable */
// @generated by swaggerz. Do not edit: this file is overwritten on the next generation.
import { type RequestOptions, client } from '../client.js';
import type { Pet } from '../models/pet.js';
import { type ApiError, type RequestError, isApiError } from '../runtime/errors.js';
import type { HttpStatus } from '../runtime/types.js';
/** Response data of {@link listPets}. */
export type ListPetsResponse = Pet[];
/** Body of an error response of {@link listPets}. */
export type ListPetsErrorBody = unknown;
/**
* The `ApiError`s {@link listPets} rejects with: one per documented status, so checking `status` narrows `body`.
*
* The last member covers statuses the specification does not document.
*/
export type ListPetsApiError = ApiError<unknown, HttpStatus<1 | 3 | 4 | 5>>;
/** Everything {@link listPets} rejects with: an `ApiError` for an error status, or a `RequestError` when no usable response arrived. */
export type ListPetsError = ListPetsApiError | RequestError;
/**
* List the pets.
*
* @remarks `GET /pets` (operationId `listPets`)
* @param options - Per-request options: `signal`, `timeout`, `headers`, `client`.
* @returns `200` The pets
* @throws {@link ApiError} When the server responds with a non-2xx status; narrow it with {@link isListPetsApiError}.
* @throws {@link RequestError} When no usable response is received (network failure, timeout or invalid body).
*/
export function listPets(options?: RequestOptions): Promise<ListPetsResponse> {
return (options?.client ?? client).request<ListPetsResponse>(
{
operationId: 'listPets',
method: 'GET',
path: '/pets',
accept: 'application/json',
},
options,
);
}
/** Whether `error` is an `ApiError` rejected by {@link listPets}. It narrows to {@link ListPetsApiError}, so checking `status` afterwards narrows `body` too. */
export function isListPetsApiError(error: unknown): error is ListPetsApiError {
return (
isApiError(error) &&
error.operationId === 'listPets' &&
error.method === 'GET'
);
}

models/pet.ts

/* eslint-disable */
// @generated by swaggerz. Do not edit: this file is overwritten on the next generation.
export type Pet = {
id?: number;
};
Generated by swaggerz 0.1.2 with { spec: { overlays: ['./vendor.overlay.yaml'] } }, at build time.