Skip to content Documentation index for agents (llms.txt)
Glotto Beta
Get started

Transforms reference

The transforms block in 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.

# 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 and 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.

rename_schema

- 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

- 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

- 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

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

Args: { path: string, name: string }path is an RFC 6901 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

- 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

- 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

- 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 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 — 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 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:

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:

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 URLextends 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:

# ✗ 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:

# ✓ 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:

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 pattern — the regex dialect RFC 9535 is defined against. So the Stainless example above generalises to every *_id parameter at once:

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 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

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.

# ./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.

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 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:

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 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.

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 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 for the full configuration surface, and the pipeline concept for where the transform stage runs.