> For the complete documentation index, see [llms.txt](/llms.txt)

# Transforms reference

The `transforms` block in [`glotto.yml`](/docs/glotto-yml) applies **in-config OpenAPI rewrites**
before the SDK is generated. They clean up a spec for better SDKs — naming anonymous schemas,
flattening compositions, fixing bad examples — **without editing your upstream OpenAPI source**.

```yaml
# glotto.yml
transforms:
  - rename_schema: { from: InvoiceDTO, to: Invoice }
  - dedupe_inline_objects: { threshold: 2 }
  - extract_ref: { path: /paths/~1pets/post/requestBody/content/application~1json/schema, name: CreatePetBody }
```

## How transforms run

- `transforms` is an **ordered list**; each entry is a single-key object `{ <name>: <args> }`,
  applied top to bottom.
- They run on the **canonical spec, before** the IR is built and any code is generated — so every
  language target sees the rewritten spec.
- Transforms are **fail-fast**: if a transform's target (a schema, a JSON-pointer, an example) does
  not exist, generation stops with an error rather than silently doing nothing. A transform that
  can't find its target is almost always a typo or a spec drift you want surfaced.

The transforms below are available today. The first six are **spec-cleanup** operations — they
make a correct spec generate a nicer SDK. The last two, [`apply_overlay`](#apply_overlay) and
[`merge_document`](#merge_document), are the **spec-correction** escape hatches for a spec that is
simply *wrong*. They differ in how a correction is addressed: `apply_overlay` targets nodes with
JSONPath, `merge_document` addresses them by structural position — see
[choosing between them](#which-correction-transform).

## `rename_schema`

```yaml
- rename_schema: { from: InvoiceDTO, to: Invoice }
```

**Args:** `{ from: string, to: string }`

Renames a component schema and **every `$ref` that points at it**. Use it to give a generated
type a clean, idiomatic name (e.g. drop a `DTO`/`Model` suffix) across the whole SDK at once.

## `flatten_composition`

```yaml
- flatten_composition: { schema: Charge.allOf }
```

**Args:** `{ schema: string }` — a dotted `"<SchemaName>.<allOf|oneOf|anyOf>"` target.

Flattens a composition keyword on a named schema into a single inline object — e.g. merges the
members of an `allOf` so the SDK emits one flat type instead of an awkward intersection.

## `dedupe_inline_objects`

```yaml
- dedupe_inline_objects: { threshold: 2 }
```

**Args:** `{ threshold: number }` — default `2`, minimum `2`.

Hoists **inline object schemas that appear `threshold` or more times** into a single named
component, replacing each occurrence with a `$ref`. This is the biggest lever for shrinking SDK
surface area: without it, codegen emits a separate anonymous type per occurrence.

## `extract_ref`

```yaml
- extract_ref:
    path: /paths/~1pets/post/requestBody/content/application~1json/schema
    name: CreatePetBody
```

**Args:** `{ path: string, name: string }` — `path` is an [RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901)
JSON-pointer into the spec (note `~1` escapes `/`), `name` is the new component name.

The targeted, one-at-a-time companion to `dedupe_inline_objects`: hoists the **single** inline
schema at `path` to `#/components/schemas/<name>` and leaves a `$ref` in its place. Use it when
you know exactly which inline schema deserves a name. Fails if `<name>` already exists.

## `fix_invalid_example`

```yaml
- fix_invalid_example: { schema: Pet, replace_with: { id: 1, name: "Rex" } }
- fix_invalid_example: { schema: Charge }   # no replace_with → removes the example
```

**Args:** `{ schema: string, replace_with?: unknown }`

Repairs a schema whose `example` is wrong (a bad example breaks generated docs and code samples).
It rewrites **only** that schema's `example` key: set it to `replace_with` when present (including
an explicit `null`), or remove the `example` entirely when `replace_with` is omitted. Fails if the
schema — or its `example` key — doesn't exist, so a stale fix never silently no-ops.

## `lift_union_members`

```yaml
- lift_union_members: { union: PaymentMethod }
```

**Args:** `{ union: string }`

Names every **inline object member of a discriminated union** — lifting each to a
`#/components/schemas/<Union><Variant>` `$ref` and extending the discriminator mapping — so the
union becomes eligible for Glotto's discriminated-union code generation.
Without it, a union whose variants are anonymous inline objects can't be narrowed by the generated
SDK. Fails fast rather than producing a partial rewrite.

## `apply_overlay`

```yaml
- apply_overlay:
    actions:
      - target: '$.paths..parameters[?(@.name == "account_id")].schema'
        description: Account IDs are strings, not numbers
        update: { type: string }
```

**Args:** an [OpenAPI Overlay 1.0.0](https://spec.openapis.org/overlay/latest.html) Overlay Object —
`actions` (≥1), plus the optional `overlay`, `info` and `extends` fields a standalone overlay
document carries, so you can paste one in verbatim. Instead of `actions`, you can point at an
overlay document you already have with [`source`](#referencing-an-overlay-document) — exactly one
of the two.

The **generic escape hatch**. The six transforms above are cleanups: they assume your spec says
what its author meant. `apply_overlay` is for when it doesn't — an auto-generated upstream source
you can't edit, a parameter typed `integer` that really returns strings, a bogus `format`. Each
action names a `target` and either **merges** an `update` into it or **removes** it.

We chose the OpenAPI Overlay standard over inventing a proprietary command vocabulary, so your
overlay knowledge is portable and your existing overlay documents mean the same thing here.

### Actions

Each entry in `actions` is `{ target, description?, update? | remove? }` — exactly one of `update`
or `remove: true`, never both and never neither.

- **`target`** — a JSONPath expression evaluated against the whole spec ([subset](#supported-jsonpath) below).
- **`description`** — free text recording *why* the correction exists. Never affects the output.
- **`update`** — when the target is an **object**, the properties to merge into it (nested objects
  merge recursively, so sibling keys survive); when the target is an **array**, a single entry to
  append.
- **`remove: true`** — delete the target from the object or array containing it. Works on any node,
  including a scalar.

Actions apply **in order**, each against the result of the last — so a later action may target
something an earlier one created.

### Referencing an overlay document

If your corrections already live in an overlay file — the form Fern's `api.specs[].overlays` uses —
reference it instead of pasting the actions in:

```yaml
transforms:
  - apply_overlay: { source: ./corrections.yaml }                     # relative to your glotto.yml
  - apply_overlay: { source: https://specs.acme.com/corrections.yaml }
  - apply_overlay:
      source:
        git: { repo: https://github.com/acme/api.git, ref: main, path: overlays/fix.yaml }
```

An entry carries **either** `source` **or** `actions`, never both and never neither. A referenced
overlay behaves exactly like an inline one from there on — same actions, same JSONPath subset, same
fail-fast on a target that matches nothing.

**Several overlays are several entries**, in the order you want them applied:

```yaml
transforms:
  - apply_overlay: { source: ./types.yaml }
  - rename_schema: { from: InvoiceDTO, to: Invoice }
  - apply_overlay: { source: ./naming.yaml }     # applies after the rename
```

A few things worth knowing:

- The document is read **on every regeneration**, not cached between runs — that is what makes it a
  standing assertion rather than a one-time patch. Editing the file changes your SDK, and
  drift detection sees it.
- A URL or git source is fetched through the same loader as `openapi.source`, with the same
  protections — private and cloud-metadata addresses are refused, redirects are capped.
- An unreachable source, or a document that isn't a valid Overlay Object, **stops the build**
  (`GLOTTO_IR_TRANSFORM_OVERLAY_SOURCE_UNREADABLE` / `GLOTTO_IR_TRANSFORM_OVERLAY_DOCUMENT_INVALID`).
  An overlay with an empty `actions` list counts as broken, not as "apply nothing".
- verification's report records each referenced overlay's origin and content hash, so the
  attestation reflects the document you actually built with — your `glotto.yml` only names it.

### `extends` is checked where it can be, and recorded where it can't

Overlay's `extends` field names the document an overlay was written for. Glotto never fetches it and
never lets it select what to overlay — that is your `openapi.source`'s job — but it is your own
assertion about which API these corrections belong to, so Glotto holds you to it where it can. It
must be an absolute URL, so a typo doesn't pass silently.

- **Your `openapi.source` is a URL** → `extends` is compared against it, ignoring the spellings that
  name the same document (a default `:443`, host case, a `#fragment`). A different document stops the
  build with `GLOTTO_IR_TRANSFORM_OVERLAY_EXTENDS_MISMATCH`, naming both URLs. If the overlay does
  belong to this API, delete `extends` — it is optional and Glotto doesn't use it.
- **Your `openapi.source` is anything else** (a local file, a git ref, a `command`) → there is no URL
  to compare against, so the assertion is carried but **not** checked. Rather than let a green build
  imply otherwise, verification's report lists it under `inputs.overlayExtends` as `unverifiable`,
  with the reason. What was checked is part of the guarantee; what wasn't is too.

### It fails when the target matches nothing

This is the part worth reading twice, and it is a **deliberate divergence from the Overlay
standard**, which says a non-matching action should be ignored.

Glotto stops the build instead. If your correction silently stopped applying the day your upstream
renamed that parameter, you'd ship an SDK with the wrong type and nothing anywhere would be red.
An overlay is a standing assertion about your spec, and we hold you to it on every regeneration —
the same fail-fast contract as the six transforms above.

The divergence is one-directional: an overlay that works here works in any conformant Overlay
tool. Only the reverse can fail, and it fails loudly, at generate time.

### Correcting a scalar

Overlay defines `update` for objects and arrays only, and Glotto doesn't invent semantics it
leaves undefined. So instead of targeting the scalar:

```yaml
# ✗ Rejected — `.type` is a string, and merging into a string has no defined meaning.
- target: '$.paths..parameters[?(@.name == "account_id")].schema.type'
  update: string
```

target the **object that contains it** and merge the key:

```yaml
# ✓ Target one segment shorter, and put the key in the update.
- target: '$.paths..parameters[?(@.name == "account_id")].schema'
  update: { type: string }
```

The error you get names the shortened target and the key to merge, so the fix is mechanical. This
is the form to use when translating a Stainless `command: update` transform, whose targets
conventionally end at the scalar.

### Supported JSONPath

`target` supports [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html):

| Construct | Example |
|---|---|
| Root | `$` |
| Child | `$.paths`, `$['paths']`, `$["paths"]` |
| Array index (negative counts from the end) | `$.servers[0]`, `$.servers[-1]` |
| Array slice (start / end / step, any of them optional) | `$.servers[1:3]`, `$.servers[-2:]`, `$.servers[::-1]` |
| Multi-selector (any selectors, applied in the order written) | `$['paths','components']`, `$.servers[0,2]` |
| Wildcard | `$.paths.*`, `$.servers[*]` |
| Descendant | `$..parameters`, `$..*` |
| Filter (both spellings) | `$..parameters[?@.in == 'path']`, `$..parameters[?(@.in == 'path')]` |
| Comparisons | `==` `!=` `<` `<=` `>` `>=` |
| Booleans, negation, grouping | `&&`, `\|\|`, `!`, `(…)` |
| Existence (the query may select many nodes) | `$..schema[?@.deprecated]`, `$..parameters[?@.examples[*]]` |
| Document-rooted operand | `$..parameters[?@.name == $.x-default-param]` |
| Functions | `length()`, `count()`, `value()`, `match()`, `search()` |
| Unicode categories in a pattern | `match(@.name, '\p{L}+')`, `search(@.name, '[\p{Lu}\p{Nd}]')` |
| Literals | `'text'`, `"text"`, `42`, `true`, `false`, `null` |

`match()` matches the **whole** string and `search()` matches **any substring**, both against an
[I-Regexp](https://www.rfc-editor.org/rfc/rfc9485.html) pattern — the regex dialect RFC 9535 is
defined against. So the Stainless example above generalises to every `*_id` parameter at once:

```yaml
transforms:
  - apply_overlay:
      actions:
        - target: "$.paths..parameters[?match(@.name, '.*_id')].schema"
          description: Every ID is a string, not a number
          update: { type: string }
```

Two things are refused rather than guessed at, and both refusals name the rule and the character
position:

- **A many-node query compared directly** — `[?@.tags[*] == 'x']`. RFC 9535 requires a comparison
  operand to be a literal or a single-node query, because "one of the tags" and "all of the tags"
  are different questions and the standard picks neither. Write `value(…)` when you mean the one
  node the query selects, or an existence test when you mean "any".
- **Regex syntax outside I-Regexp** — backreferences, lookahead, lazy quantifiers (`a*?`), the
  anchors `^` / `$` (`match()` is already whole-string, so use `search()` for a substring), and
  Unicode **block** escapes such as `\p{IsBasicLatin}`, which I-Regexp excludes in favour of
  categories. These would all mean something different — or nothing at all — in another Overlay
  tool.

`\p{…}` and `\P{…}` **Unicode category** escapes are supported — the one-letter groups `L`, `M`,
`N`, `P`, `Z`, `S`, `C` and the two-letter categories they cover (`Lu`, `Ll`, `Nd`, `Pc`, `Sc`, …),
with `\P{…}` as the complement. They resolve against a Unicode table Glotto vendors and pins, not
against the runtime's, so the same `target` selects the same nodes on your machine and in the
hosted build — which is the whole reason they can be offered at all.

`\d`, `\s` and `\w` stay **ASCII** (`[0-9]`, `[ \t\n\r]`, `[0-9A-Za-z_]`). I-Regexp omits these
classes precisely because they vary so much between regex flavours, and
[RFC 9485 §5](https://www.rfc-editor.org/rfc/rfc9485.html) recommends `[0-9]` for `\d` rather than
the broader Unicode reading. When you want the Unicode reading, write it: `\p{Nd}` matches a digit
in any writing system, `\p{L}` any letter.

Anything else outside the grammar is likewise a **parse error naming the construct and its
position**, never a silent non-match. A JSONPath that *almost* parses would select the wrong nodes
and corrupt your spec in a way nothing downstream could trace back; refusing costs you an error
message instead.

### Not yet supported

A fetched overlay document is **not cached between runs** — an overlay is a standing assertion, so
it is re-read and re-checked on every regeneration. The six cleanup transforms take scalar
arguments and so have no `source:` form; there is no document to reference.

## `merge_document`

```yaml
transforms:
  - merge_document: { source: ./overrides.yml }
```

A **merge document** is a partial OpenAPI document — structured exactly like the real one — that is
deep-merged over your spec. Where `apply_overlay` says *"find this node and change it"*, a merge
document says *"here is the shape I want; merge it in"*.

It is the shape you already have if you arrived from Fern (`api.specs[].overrides`), or if your
team keeps corrections as a partial spec applied with a merge tool. migrating from Fern carries
those files across as `merge_document` entries — see [migrating from
Fern](/docs/migrate-from-fern).

```yaml
# ./overrides.yml — only the parts you're correcting.
components:
  schemas:
    Account:
      properties:
        id:
          description: The account's opaque identifier.
```

### The document, inline or referenced

Exactly one of `document:` (written inline) or `source:` (referenced) — never both, never neither.

```yaml
transforms:
  - merge_document: { document: { info: { termsOfService: https://acme.com/terms } } }
  - merge_document: { source: ./overrides.yml }                       # relative to your glotto.yml
  - merge_document: { source: https://specs.acme.com/overrides.yml }
  - merge_document:
      source:
        git: { repo: https://github.com/acme/specs, ref: main, path: overrides.yml }
```

Several documents are **several entries**, in the order you write them — the same rule
`apply_overlay` follows, and for the same reason: `transforms` is ordered, and the order is
semantic. A referenced document goes through the same loader as your input spec, so it inherits
the same URL and git-ref safety checks.

### Merge rules

- **Objects merge recursively** — a key you don't mention is left alone.
- **Arrays replace**, they do not append. There is no way to say "the third one" in a merge
  document; if you need positional addressing, that is what `apply_overlay` is for.
- **Scalars replace**, including an explicit `null`. A `null` sets the key to null; it does **not**
  delete it. (That is [JSON Merge Patch](https://www.rfc-editor.org/rfc/rfc7396) behaviour, which
  is deliberately *not* what this is — to delete a node, use `apply_overlay`'s `remove: true`.)
- **A key your spec doesn't have is added**, creating intermediate objects as needed. This is the
  thing `apply_overlay` cannot do, and the reason both exist.

### Anchoring the document with `at:`

A correction to a deeply-nested schema costs you the whole nesting on the way down. `at:` names
the address once, and the document beneath it is written against that node:

```yaml
transforms:
  # Full shape — five levels of nesting restated to correct one field.
  - merge_document:
      document:
        components:
          schemas:
            Account:
              properties:
                id:
                  description: The account's opaque identifier.

  # Anchored — the same correction, addressed once.
  - merge_document:
      at: $.components.schemas.Account.properties.id
      document:
        description: The account's opaque identifier.
```

The two produce **byte-identical** specs. `at:` is a JSONPath expression in the same
[supported subset](#supported-jsonpath) `apply_overlay`'s `target:` uses, evaluated against the
spec as it reaches this entry.

One rule comes with it: **an anchor is an address, so it must resolve, and to exactly one node.**

- Matching **nothing** stops the build with `GLOTTO_IR_TRANSFORM_TARGET_NOT_FOUND`. An anchor never
  creates the node it names: `at: $.components.securitySchemes` against a spec that has no
  `securitySchemes` is an error, not an invitation. Creating a node that deep is what the
  full-shape form is for, and it still does.
- Matching **two or more** nodes stops the build with `GLOTTO_IR_TRANSFORM_TARGET_AMBIGUOUS`. A
  merge document is a correction to *one* place — it is written against a single node's shape — so
  a wildcard that fanned out is far more often a typo than an intent. When you do want one rule
  applied to every match, that is `apply_overlay`, which broadcasts by design.

Beneath a resolved anchor nothing else changes: the merge rules above apply unaltered, a key the
node doesn't have is still added, and the inert check below still looks at the whole spec.

### Asserting a document only corrects, with `strict:`

Adding a key is what makes a merge document more expressive than an overlay — and it is also how a
typo disappears. Write `descriptoin` and the misspelling is added as a brand-new node: your
correction never lands, nothing is red, and the mistake ships into every SDK.

`strict: true` says *this document only corrects; it never extends*. Every **leaf** of the
document — every scalar, array and `null`, plus any empty object — must already exist at the
corresponding path in your spec.

```yaml
transforms:
  - merge_document:
      strict: true
      document:
        components:
          schemas:
            Account:
              properties:
                id:
                  descriptoin: The account's opaque identifier.
```

```
transforms[0] 'merge_document': `strict: true` asserts every leaf of this document already
exists, but the spec does not have 1 of the paths its leaves name:
components.schemas.Account.properties.id.descriptoin. Fix each path — a misspelled key is the
usual cause — or drop `strict:` if this entry is meant to extend the spec rather than only
correct it.
```

A leaf whose path does not resolve stops the build with
`GLOTTO_IR_TRANSFORM_MERGE_DOCUMENT_STRICT_VIOLATION`. Three things to know about it:

- **Every offending path is listed, not just the first** — a hand-written override usually has more
  than one typo, and one per run is the slowest possible loop.
- **The check runs before the merge**, so a violation leaves your spec exactly as it was. Nothing is
  half-applied.
- **It composes with `at:`.** Leaf paths are resolved relative to the anchor, and reported that way.

`strict:` defaults to `false`, so every existing entry keeps adding as it always has. It is also
independent of the inert check below: the two ask different questions and can disagree about the
same document. A document that replaces one leaf with the value already there is strict-clean and
inert-failing; a document that adds one key and replaces another is inert-clean and
strict-failing.

### It fails when the document changes nothing

If merging your document produces a spec identical to the one it was applied to, generation
**stops** with `GLOTTO_IR_TRANSFORM_MERGE_DOCUMENT_INERT`.

That means your upstream has caught up: it now asserts everything your override was correcting, so
the override is dead. Left alone, a dead override is indistinguishable from a live one — it sits in
your config forever, and nobody finds out if the upstream later reverts. Delete the entry (or point
it at the document you meant).

This is checked **per entry, not per key**: a document whose ten keys include one that still
changes something is live, so keeping a deliberately complete override is fine.

## Which correction transform?

| You have… | Use |
|---|---|
| An [OpenAPI Overlay](https://spec.openapis.org/overlay/v1.0.0.html) document (`actions[]`) | `apply_overlay` |
| A partial OpenAPI document to merge in | `merge_document` |
| A correction that must **add** a node your spec lacks | `merge_document` |
| A correction that must **delete** a node | `apply_overlay` (`remove: true`) |
| A correction addressing "every parameter named X", or one array element | `apply_overlay` |
| A deep correction you'd rather not restate the nesting for, at **one** node | `merge_document` with `at:` |
| One rule to apply at **every** node an expression matches | `apply_overlay` — a multi-match `at:` is an error, not a broadcast |
| A correction that must only **fix** existing values, never add new ones | `merge_document` with `strict: true` |
| A Fern project's `overlays:` / `overrides:` | one each — migrating from Fern picks correctly |

Both are ordered entries in the same `transforms` list, so you can mix them freely.

## Where transforms sit

`transforms` is an optional top-level key in `glotto.yml`; see the
[`glotto.yml` reference](/docs/glotto-yml) for the full configuration surface, and the
[pipeline](/docs/pipeline) concept for where the transform stage runs.
