glotto.yml model shaping
Model shaping
models
models:
WidgetsCreateWidgetRequestAddress:
name: Address # rename the model, in every target and every artifact
Coordinates:
name: { go: Coords } # or pin the name per target
TinyWrapper:
inline: true # don't emit a standalone type — inline it at each reference
DebugInfo:
inline: { go: true } # or un-promote it for specific targets only
Shape the models Glotto emits. Two directives, each taking a single value (applying everywhere) or an object keyed by target:
namerenames a model. This is the escape hatch for the names Glotto synthesizes: an inline object schema — say anaddressproperty on a request body — is promoted to a named model so every language can type it, and its name is derived from where it sits in the spec (WidgetsCreateWidgetRequestAddress). Rename it to whatever reads well. Only the emitted type name changes; the JSON payload is untouched.inlinedoes the opposite: it un-promotes a model, substituting its body wherever it’s referenced and emitting no standalone type. Useful for a small one-off wrapper that isn’t worth a named type of its own.
A name set as a single value applies to every artifact — the SDKs, the docs site, the MCP server, the mock server and the Terraform provider all show it — so one model has one name everywhere. The per-target object form is applied per SDK instead, leaving the shared name in place elsewhere.
An entry naming a model your spec doesn’t produce is ignored, so a config can outlive a schema
change. A few directives are declined so generation stays correct: renaming onto a name another model
already holds (it would merge two types), and inlining a model that refers back to itself or that is
a member of a discriminated union. In each case the model simply stays as it was — and both
glotto generate tells you so, naming the directive’s path and why it was
declined (GLOTTO_CONFIG_MODEL_SHAPING_REFUSED).
Migrating from Stainless? Glotto translates x-stainless-model and
x-stainless-model-skip into this block for you.
Renaming a model for one language
name’s object form is also where you settle a name collision with the SDK’s own code. Some type
names are unusable in a given language because the generated SDK already refers to them: a model
called Data in a Swift SDK would land in the same module as the client’s own Data references and
take them over, and Swift offers no way to disambiguate a same-module declaration.
Glotto handles this for you. A colliding model is emitted under a suffixed name — Data →
DataModel, and DataModel2 if that is taken too — in that target only, with every reference
updated to match, and glotto generate prints a
GLOTTO_CONFIG_MODEL_RENAMED warning saying
which name it chose. Your other SDKs keep the original name.
To choose the name yourself instead, pin it per target — an explicit name always wins, and the warning stops:
models:
Data:
name: { swift: Payload } # Swift emits `Payload`; every other target still emits `Data`
The warning opens with the exact key to set, so you can read it straight out of the output — and it
stays the key you wrote even if another models entry has already renamed that model.
There is no separate model_names block: this is that setting. Use transforms with rename_schema
if you want the model renamed in every target and artifact rather than just one.
enums
enums:
Status:
nominal: false # emit a plain alias over the primitive, not a named type
Kind:
nominal: { go: false } # or just for specific targets
Choose an enum’s typing shape: its own named type, or a plain alias over the primitive it’s carried by.
By default Glotto gives an enum its own type in every language that can express one — a
#[non_exhaustive] enum in Rust, a RawRepresentable struct in Swift, an extension type in Dart,
a StrEnum subclass in Python (an IntEnum, or a float-backed enum, when the enum’s values are
numbers), a defined type Status string in Go. Set nominal: false and it becomes an alias instead
(pub type Status = String, typealias Status = String, type Status = string, …), over whichever
primitive the enum is actually carried by — so a numeric enum aliases to i64 / Int / int /
Python’s int (or float for decimal values), not to a string type.
Reach for it when a named type gets in the way rather than helping: your callers would rather pass a plain string than import and construct a type; you’re moving from a hand-written SDK where the field was a string and promoting it would break your users; or the enum’s members change often, so the named type churns your public API on every regeneration while an alias doesn’t.
The trade-off is worth stating plainly: an alias has no members, so you give up the generated
constants (Status.active, Status::Active) and callers write the raw string. Nothing changes on
the wire — the value is the same primitive either way — so switching shape is not an API
re-versioning event.
This applies to the six targets whose language has a type-alias construct: Go, Rust, Swift, Dart,
Kotlin and Python. Java, PHP, C# and Ruby have no alias construct to emit, and TypeScript,
React Native and Elixir have no nominal form to choose against (TypeScript is structurally typed, so
its emitted type Status = 'active' | 'archived' is already an alias). glotto generate warns if you
address one of them per target.
nominal: true states the default explicitly. It’s accepted and carried, but it doesn’t change
emission: where a named type is available it’s already what you get, and where one isn’t — a
mixed enum (say ['a', 1, true], which has no single primitive to be typed over) or a
boolean one in Rust, Swift or Dart — Glotto declines rather than inventing a shape it can’t
stand behind.
Migrating from Stainless? Glotto translates x-stainless-nominal into
this block. Note the defaults are opposite — Stainless aliases by default, Glotto names by default —
and only enums you actually annotated are translated, so review the ones you didn’t.
soft_required
soft_required:
listInvoices:
parameters: [account_id] # the SDK method demands this query/header parameter
createInvoice:
body: true # …and this operation's request body
body_fields: [currency] # …and this field of the request-body model
Mark inputs your API accepts without, but that every real caller should send. The generated SDK
method demands them — a Python keyword with no default rather than = None, a Go string value
rather than a *string, a TypeScript member without its ? — so the ergonomics guide the caller
toward the call you actually want.
What makes this different from editing required in your spec is everything it doesn’t touch.
Your docs reference still lists the parameter as optional, the generated MCP server still marks it
optional in its tool schema, the generated mock server still accepts a request that omits it, and
Breaking-change detection reports nothing — because none of those describe the SDK signature,
they describe your protocol, and your protocol hasn’t changed. Adding an entry here is never a
breaking change.
Entries are keyed by operationId, and one naming an operation, parameter, or field your spec
doesn’t carry is ignored, so a config can outlive a schema change.
One case is handled on a copy so the generated SDK stays correct: a body_fields entry on a model
your API also returns. Requiredness there is the same flag the SDK’s decoder reads, so demanding
the field on the shared type would make responses that omit it fail to parse — a preference turned
into a runtime error on data you don’t control. Instead the SDK gains a request variant of that
model, named <Model>Request, and only the operation you named uses it:
export interface Invoice { id: string; memo?: string } // what you receive
export interface InvoiceRequest { id: string; memo: string } // what createInvoice demands
createInvoice(params: InvoiceRequest): Promise<Invoice>
Every other reference — responses, nested fields, and other operations’ bodies — keeps using the
original model, so nothing that parses your API’s output changes. If the name is already taken, the
variant becomes <Model>Request_2; renaming the model itself with the models block
carries through (Invoice → Bill yields BillRequest).
Because that adds a type you didn’t name, glotto generate tells you it
happened, as a GLOTTO_CONFIG_SOFT_REQUIRED_SPLIT
warning naming the operation, the field, the shared model and the variant — so a new SDK type never
appears unexplained. (An entry your spec doesn’t carry stays silently ignored, as above: that’s the
stale-config case, not a promotion that moved.)
Migrating from Stainless? Glotto translates x-stainless-soft-required
into this block for you.
auto_populate
auto_populate:
createCustomer:
parameters: [api_version] # the SDK sends this query/header parameter itself
body_fields: [object] # …and this field of the request body
The inverse of soft_required. Where that block adds an input your protocol
doesn’t demand, this one removes an input your protocol does demand — because there is only
one value it could ever hold, and the SDK can supply it.
The shape this is for is the Stripe-style discriminant: object: { type: string, enum: ["customer"] }
on a request body, or a pinned api_version query parameter. The schema permits exactly one value,
and every caller types it at every call site anyway. Name the position here and the generated method
drops it from its signature, while the generated request still sends it:
// before
createCustomer({ object: 'customer', email: 'a@b.com' })
// after
createCustomer({ email: 'a@b.com' })
Entries are keyed by operationId. parameters names query and header parameters by their wire
name; body_fields names properties of the request-body object. All 13 languages honour it, and in
each one the value is injected at the same place the per-call
extraQuery / extraHeaders / extraBody escape hatch merges
— so a caller who passes an explicit value for the same key still wins. The constant is the
base, never an override.
Like soft_required, this changes only the SDK signature. Your docs reference still documents the
field, the generated MCP server still exposes it, the generated mock still accepts it, and
Breaking-change detection reports nothing — none of those describe the SDK signature, they
describe your protocol, and your protocol hasn’t changed.
A position is only eligible if its schema permits exactly one value with a sendable wire form.
Naming one that doesn’t — a multi-value enum, a null-only schema, a field of a non-object body —
is reported by glotto generate as a
GLOTTO_CONFIG_AUTO_POPULATE_REFUSED
warning naming the position and the rule it failed, rather than being silently skipped. (An entry
naming an operation or field your spec doesn’t carry stays silently ignored, as elsewhere: that’s
the stale-config case, not a directive that was declined.)
One case is handled on a copy. If the request body resolves to a model your API also returns, or
that another operation sends without asking for the same removal, removing the field in place would
be lossy — the response type would lose a field the server still sends, or the other operation’s
signature would lose an input with nothing to re-inject it. Instead the SDK derives a
<Model>Request variant carrying the removal, and only the operations you configured point at it.
Unlike the soft_required split there is nothing to announce: the field is gone from the
signature either way, so the variant never appears in a call site.
Migrating from Stainless? Glotto translates x-stainless-const into
this block for you.