SDK generation diagnostics
SDK generation
Raised while a codegen engine emits an SDK, when the engine can tell in advance that the source it would write cannot compile. Generation stops rather than writing it, so the failure names your spec instead of the SDK’s own runtime.
These stop the one target, not the whole run. When an engine refuses, glotto generate drops
that target and still writes every other one — a refused Swift SDK does not cost you your TypeScript
and Python SDKs. The run exits non-zero and names the target it could not build, so a CI pipeline
that asked for thirteen SDKs and received twelve fails rather than passing quietly.
glotto generate reports these before anything is written, so you can see the problem
without waiting for a build.
Reserved model names
A schema whose type name collides with a type the generated SDK itself uses is a family of its own, with a row for every language and a full account of what each engine reserves: Reserved model names. The rest of this page is about a collision between two of your own schemas, which is a different question with a different remedy.
Two of your own schemas emitting one type name
A separate problem with a separate code, and no name of the SDK’s involved. C# writes each model to
its own Glotto/<Name>.cs and derives that name in PascalCase, which is lossy — so two schemas that
differ only in casing or separators (widget and Widget, foo_bar and FooBar) emit one type
name and claim one file. Left alone, whichever file is written last destroys the other, the SDK
builds cleanly, and one of your schemas is simply not in it.
| Code | Language | When you see it |
|---|---|---|
GLOTTO_CSHARP_MODEL_NAME_COLLISION |
C# | Two or more of your schemas emit the same C# type name, and a models: { … : { name: { csharp: … } } } pin sits on the one generation would otherwise rename |
Generation resolves this for you in every other case. The schema whose name is already spelled
exactly as the type it emits keeps that type name, and each other takes the model suffix — FooBar
and fooBar become FooBar and FooBarModel. Where no schema is spelled that way, the first key in
code-point order keeps it — and generation reports every rename it applied, exactly as it does for a reserved
model name. Only the C# type name changes; your other SDKs keep both original
names. Pinning the schema that already keeps its spelling does not trigger this diagnostic: that
schema was never going to move, so the pin asks for nothing generation was not already doing.
A pin is a decision, so it is never renamed over. Where the schema generation would otherwise
RENAME got its name from models.<M>.name.csharp, the C# target is refused instead: renaming over
an explicit pin would give you a type name you did not ask for. Give one of the two a different C#
name, or rename one schema everywhere with a rename_schema transform.
Every other target still generates.
Two of your own schemas sharing one name
Every code above is about a collision with something Glotto declares. This one is not: it fires when two schemas you wrote would emit the same type, so neither name is reserved and there is nothing of ours in the way.
| Code | Language | Fires when | Resolved automatically? |
|---|---|---|---|
GLOTTO_PHP_MODEL_NAME_COLLISION |
PHP | Two schema names emit one PHP class — foo_bar, fooBar, foo-bar and FooBar all emit class FooBar, and PHP class names are case-insensitive, so widget and WIDGET are one class too |
Yes — the later ones become FooBarModel, FooBarModel2, … and every rename is reported. You only see the diagnostic if the rename cannot be applied |
PHP gives every model its own file, so before the rename existed the second schema’s file simply
replaced the first on disk and the model was gone — with php -l reporting the survivor clean, the
SDK loading without complaint, and its own test suite passing. That is the failure the automatic
rename removes.
Two situations still stop the PHP target rather than renaming, and both raise the code above. The
first is a pin you cannot have: if you set models: { A: { name: { php: FooBar } } } while
another of your schemas already emits FooBar, generation refuses instead of quietly giving your
pinned model a different name — a pin is a decision, not a suggestion. Rename either schema, or drop
the pin and let the automatic rename resolve it. The second is a collision the rename does not yet
cover — a schema named after a discriminated-union variant class, such as PetCat for a Pet
union with a cat discriminator value. Generation stops and names both files rather than writing an
SDK with one of the declarations deleted out of it; renaming the schema resolves it.
When two of your own schemas collide
Everything above is a schema colliding with a name the SDK itself puts in scope. Two of your own
schemas can collide the same way, and Glotto resolves that by renaming too — the second schema keeps
its data, its fields and its file, under a <Name>Model type name that the generate run reports.
Nothing is dropped, and no target is skipped.
There is one case it cannot resolve for you: when you pinned the name it would have to move.
| Code | Language | Raised when | How to resolve it |
|---|---|---|---|
GLOTTO_RUBY_MODEL_NAME_COLLISION |
Ruby | You set models.<model>.name.ruby to a Ruby name another of your schemas already emits. Ruby’s constant and file-name casing merge spellings your spec keeps apart — FooBar, foo_bar, fooBar and foo-bar all emit Glotto::FooBar into lib/glotto/foo_bar.rb, and foofoo / FOOfoo share the file while FOOFoo / FOO_foo share the constant, so two names that look distinct to you can be one name to Ruby |
Rename either schema: pick a different Ruby name for the pinned one with models.<model>.name.ruby, or give the other schema a Ruby name of its own the same way. Removing the pin also resolves it — without one the collision is renamed automatically. Only the Ruby target is affected; every other SDK in the same run still generates |
GLOTTO_RUBY_UNIT_CONSTANT_COLLISION |
Ruby | Two files of the generated Ruby gem would declare the same constant at the top of the Glotto module — for example a schema whose name lands on one of the SDK’s own classes after the automatic rename has already run. Ruby does not refuse a repeated constant: it reopens the first declaration and MERGES the second into it, so the gem would load, the generated tests would pass, and the class would be half your schema and half the SDK’s. Generation stops instead |
Rename the schema that collides with models.<model>.name.ruby, choosing a name no other schema and none of the SDK’s own classes use. The message names both files, so the second path tells you which of your schemas is involved. Only the Ruby target is affected; every other SDK in the same run still generates |
A pin is treated as a decision rather than a suggestion, which is why this is reported instead of renamed: quietly emitting your pinned schema under a different name would substitute a type name you never asked for. A pin on the schema that keeps its name is never reported — that one generates exactly as you pinned it.
Two generated Kotlin types that claim one file
| Code | Meaning |
|---|---|
GLOTTO_KOTLIN_EMITTED_PATH_COLLISION |
Two types the Kotlin SDK generates from your spec would be written to the same file, and neither of them is a schema, so there is no model for the automatic rename above to move. The message names both keys that produced it. Two cases you can actually hit. (1) An operation with an inline JSON or multipart request body mints <Resource><Method>Request, and a discriminated union mints <Union><Value> from your own discriminator values — so a pets.add operation beside a union named PetsAdd that maps the value request both mint PetsAddRequest. (2) Two of your keys that differ only in CASE mint two type names that differ only in case — resources keyed widget and WIDGET, each with an inline request body, mint WidgetAddRequest and WIDGETAddRequest, and a JVM class file is named after the type rather than the source file, so the two claim one .class path however the sources are named. Rename the union, the discriminator value, the resource, or the operation; any one of the four resolves it. Only the Kotlin target drops out — every other target you configured still generates. |
Kotlin is the one language where two generated types can claim one file without a schema being
involved, because it writes one file per top-level type and names each file after the type. The same
property is why the Kotlin reserved set above compares the SDK’s own type names
case-insensitively while leaving List, Map and Any alone: Client and CLIENT are two
perfectly good Kotlin types, but they compile to Client.class and CLIENT.class, which are one
file on macOS and Windows. That build is green and the class is simply absent, so a schema named
CLIENT is renamed to CLIENTModel rather than left for a compiler that will not complain.
A separate problem with a separate code, and no name of the SDK’s involved. Java writes each model
to its own com/glotto/<Name>.java and derives that name in PascalCase, which is lossy — so two
schemas that differ only in casing or separators (widget and Widget, foo_bar and FooBar)
emit one type name and claim one file. Left alone, whichever file is written last destroys the
other, the SDK builds cleanly, and one of your schemas is simply not in it.
| Code | Language | When you see it |
|---|---|---|
GLOTTO_JAVA_MODEL_NAME_COLLISION |
Java | Two or more of your schemas emit the same Java type name, and you pinned the one Glotto would have renamed with models: { … : { name: { java: … } } } |
Generation resolves this for you when you have not pinned a name. The schema whose name is
already spelled exactly as the type it emits keeps that type name, and each other takes the model
suffix — FooBar and fooBar become FooBar and FooBarModel. Where no schema is spelled that
way, the first key in code-point order keeps it — and generation reports every rename it applied, exactly as it does for a
reserved model name. The order your schemas appear in your document never
decides this, so re-ordering two of them never moves a type name. Only the named target’s type name
changes; your other SDKs keep both original names.
A pin is a decision, so it is never renamed over. Where the schema the rename would have moved
got its name from models.<M>.name.<target>, that one target is refused instead: renaming over an
explicit pin would give you a type name you did not ask for. Give one of the two a different name
for that target, or rename one schema everywhere with a
rename_schema transform. Every other target still generates.
The same collision in every other target
The rule above is not C#’s, Java’s, PHP’s or Ruby’s — it is one rule, and every target Glotto generates applies it. Six more raise their own code for the pinned case:
| Code | Language | What the collision would have done, measured |
|---|---|---|
GLOTTO_DART_MODEL_NAME_COLLISION |
Dart | dart analyze reports The name 'FooBar' is already defined and the SDK does not analyze |
GLOTTO_ELIXIR_MODEL_NAME_COLLISION |
Elixir | Nothing at all. elixirc exits 0 with no diagnostic and the second defmodule simply replaces the first, so one of your schemas is gone from a build that reported success |
GLOTTO_GO_MODEL_NAME_COLLISION |
Go | go vet reports FooBar redeclared in this block and the SDK does not build |
GLOTTO_PYTHON_MODEL_NAME_COLLISION |
Python | Nothing at all. Importing the SDK succeeds and the second class rebinds the name, so one of your schemas is gone with no error from any tool |
GLOTTO_RUST_MODEL_NAME_COLLISION |
Rust | cargo check reports error[E0428]: the name \FooBar` is defined multiple times` and the SDK does not build |
GLOTTO_SWIFT_MODEL_NAME_COLLISION |
Swift | swiftc reports invalid redeclaration of 'FooBar' and every use site becomes ambiguous, so the SDK does not build |
GLOTTO_KOTLIN_MODEL_NAME_COLLISION |
Kotlin | kotlinc writes one file per top-level type, so the two claim one path and one is discarded before the compiler runs. Distinct from GLOTTO_KOTLIN_EMITTED_PATH_COLLISION, which is the case where neither name is a schema of yours |
Two of those six say nothing, and that is the reason the rename exists. In Elixir and Python the generated SDK builds, imports, and passes its own emitted tests with one of your schemas missing — there is no error to look for, and no exit code that would tell you. The four that fail loudly are the easy half.
Everything in the two paragraphs above applies unchanged to all of them: the collision is renamed automatically and every rename is reported, only the affected target’s type name moves, and a pin is refused rather than renamed over.
Two Dart resource groups sharing one class name
| Code | Cause | Remedy |
|---|---|---|
GLOTTO_DART_RESOURCE_NAME_COLLISION |
Distinct resource grouping keys project to the same class in the generated Dart library, for example 0 and resource_0 both produce Resource0Resource |
Give the resource groups distinct names while preserving their operations’ HTTP paths. A model-name override cannot rename a resource class. Omit dart from targets to continue generating your other SDKs while resolving the grouping collision. |
A property name Go cannot carry
Go is the one target that stores a field’s wire name in a struct tag — `json:"color"` — and
neither the tag’s own quoting nor encoding/json’s tag grammar can hold an arbitrary string. A tag
carries a name built from letters, digits, and the punctuation !#$%&()*+-./:;<=>?@[]^_{|}~ plus
the space; any other character is unrepresentable. Among printable ASCII that is these five, each
measured against the pinned Go toolchain rather than read off the standard library:
| character in the name | what Go does with it |
|---|---|
` (backtick) |
Closes the struct tag literal. The generated file does not parse |
" (quote) |
The tag parses, the SDK builds, and the field (de)serializes under a truncated key — quo"te goes on the wire as quo |
\ (backslash) |
The same truncation — back\slash goes on the wire as back |
, (comma) |
The same truncation, because a comma separates tag options — com,ma goes on the wire as com |
' (apostrophe) |
The same truncation. It is none of the characters the tag’s own quoting reserves — it is simply outside the grammar — so a'b goes on the wire as a |
One whole name is refused for a different reason: a property named exactly -, which
encoding/json reads as skip this field, so the field would leave the wire altogether rather
than under a truncated key. A dash inside a name is fine, as are a space, a dot and $ — this
is not a rule about unusual-looking names.
| Code | Meaning |
|---|---|
GLOTTO_GO_UNREPRESENTABLE_WIRE_NAME |
A property name (or union discriminator) in your spec contains a character a Go struct tag cannot carry, so the Go SDK would either fail to build or silently read and write the wrong key. The Go target is not generated and every offending property is named with its character; every other configured target still emits, and this is reported before any files are written. Rename the property in your spec, or drop go from this config’s targets — every other language carries these names without difficulty |
A request body the SDK cannot encode
Go and Python send a request body as JSON, a raw string or bytes, multipart, or
application/x-www-form-urlencoded. A form body is a set of name=value pairs, so its schema must
be an object or a map. An operation whose body is none of these, such as application/xml over an
object schema, a form body declared as a scalar or array, or a multipart body that is not an
object, would either have no method in that SDK or, for a paginated or streaming operation, be sent
with the wrong body. It is refused instead. Every other SDK that sends a form body refuses one
declared over anything but an object or a map the same way, each under its own code below.
The TypeScript and React Native SDKs also send an application/x-www-form-urlencoded body from the
typed object or map, and refuse a form body declared as anything else.
Java and Kotlin take a form operation’s model as its body and encode it themselves, so for them only a form body that is neither an object nor a map has nothing to take, and is refused the same way.
| Code | Meaning |
|---|---|
GLOTTO_GO_UNENCODABLE_REQUEST_BODY |
At least one operation’s request body cannot be encoded by the Go SDK. The Go target is not generated. Each offending operation is named with its method, its path and the reason. Every other configured target still emits, and this is reported before any files are written. Declare the body with a media type and schema Go can send (JSON, a string-typed text body, a form or multipart body over an object, or application/octet-stream), or drop go from this config’s targets |
GLOTTO_PYTHON_UNENCODABLE_REQUEST_BODY |
The same refusal for the Python target. The Python target is not generated, each offending operation is named with its reason, and every other configured target still emits. Declare the body with a media type and schema Python can send, or drop python from this config’s targets |
GLOTTO_TYPESCRIPT_UNENCODABLE_REQUEST_BODY |
At least one operation declares application/x-www-form-urlencoded over a schema that is neither an object nor a map, so the TypeScript SDK has no name=value encoding for it. The TypeScript target is not generated, each offending operation is named with its method, its path and the reason, and every other configured target still emits. Declare the form body as an object or a map, or drop typescript from this config’s targets |
GLOTTO_REACT_NATIVE_UNENCODABLE_REQUEST_BODY |
The same refusal for the React Native target. The React Native target is not generated, each offending operation is named with its reason, and every other configured target still emits. Declare the form body as an object or a map, or drop react_native from this config’s targets |
GLOTTO_JAVA_UNENCODABLE_REQUEST_BODY |
At least one operation’s request body is application/x-www-form-urlencoded over a schema that is neither an object nor a map, so the Java SDK has no model to take as its body and nothing to encode. The Java target is not generated, each offending operation is named with its method, its path and the reason, and every other configured target still emits. Declare the form body as an object or a map, or drop java from this config’s targets |
GLOTTO_KOTLIN_UNENCODABLE_REQUEST_BODY |
The same refusal for the Kotlin target, on all three packaging variants (JVM, Android and Multiplatform). The Kotlin target is not generated, each offending operation is named with its reason, and every other configured target still emits. Declare the form body as an object or a map, or drop kotlin from this config’s targets |
GLOTTO_CSHARP_UNENCODABLE_REQUEST_BODY |
At least one operation declares application/x-www-form-urlencoded over a schema that is neither an object nor a map, which the C# SDK cannot encode as name=value pairs. The C# target is not generated, each offending operation is named with its method, its path and the reason, and every other configured target still emits. Declare the form body as an object or a map, or drop csharp from this config’s targets |
GLOTTO_PHP_UNENCODABLE_REQUEST_BODY |
The same refusal for the PHP target. A form-urlencoded body over a schema that is neither an object nor a map cannot be sent as name=value pairs. The PHP target is not generated, each offending operation is named with its reason, and every other configured target still emits. Declare the form body as an object or a map, or drop php from this config’s targets |
GLOTTO_RUBY_UNENCODABLE_REQUEST_BODY |
The same refusal for the Ruby target. A form-urlencoded body over a schema that is neither an object nor a map cannot be sent as name=value pairs. The Ruby target is not generated, each offending operation is named with its reason, and every other configured target still emits. Declare the form body as an object or a map, or drop ruby from this config’s targets |
GLOTTO_RUST_UNENCODABLE_REQUEST_BODY |
An operation declares an application/x-www-form-urlencoded request body whose schema is neither an object nor a map, which the Rust SDK cannot encode as name=value pairs. The Rust target is not generated, each offending operation is named with its method, its path and the reason, and every other configured target still emits. Declare the form body as an object or a map, or drop rust from this config’s targets |
GLOTTO_SWIFT_UNENCODABLE_REQUEST_BODY |
The same refusal for the Swift target: an application/x-www-form-urlencoded request body whose schema is neither an object nor a map. The Swift target is not generated, each offending operation is named with its method, its path and the reason, and every other configured target still emits. Declare the form body as an object or a map, or drop swift from this config’s targets |
GLOTTO_DART_UNENCODABLE_REQUEST_BODY |
The same refusal for the Dart target: an application/x-www-form-urlencoded request body whose schema is neither an object nor a map. The Dart target is not generated, each offending operation is named with its method, its path and the reason, and every other configured target still emits. Declare the form body as an object or a map, or drop dart from this config’s targets |
GLOTTO_ELIXIR_UNENCODABLE_REQUEST_BODY |
The same refusal for the Elixir target: an application/x-www-form-urlencoded request body whose schema is neither an object nor a map. The Elixir target is not generated, each offending operation is named with its method, its path and the reason, and every other configured target still emits. Declare the form body as an object or a map, or drop elixir from this config’s targets |
A paginated operation whose success is not a JSON document
Every SDK Glotto generates ships its own integration-test suite, and for an operation paginating by
cursor or cursor id that suite walks two real pages. Building it needs the operation’s documented
success to be a JSON document in which your locators actually resolve: itemsPath, plus
nextCursorPath or lastItemIdPath. Those always come from the operation’s
x-pagination extension — a query-parameter name can tell Glotto that an operation paginates, but
never where its items or its next cursor live, so this diagnostic only reaches operations whose
extension names them. Pagination covers the SDK surface they produce.
This one stops every target, not one. The plan behind those suites is language-agnostic and is built once for all thirteen SDKs, so the run names each target with the same message and writes nothing. That is the right reading: what it reports is a disagreement between the locators you declared and your own response schema, and no single language is involved.
A mixed success is fine — an operation documenting both a JSON page and a binary download at the
same status paginates over the JSON side, and the generated mock server answers it with that side
too. What cannot work is a success with no JSON side at all: an application/octet-stream body is an
opaque byte stream, and the locators exist nowhere in it. Generation refuses that combination for
cursor and cursor-id pagination; on the page, offset and link-header strategies it is not yet
detected.
| Code | Meaning |
|---|---|
GLOTTO_PAGINATION_LOCATORS_UNRESOLVED |
An operation paginates, but its documented success is not a JSON document the locators resolve in. The message names the operation, the locator, and what was found there — a path that resolves to no field, a field that is not an array, an item schema that does not resolve, a cursor that samples to the wrong kind of value, or a success classified as a binary download, which declares no JSON model at all. Fix whichever of the two is wrong: correct the locator in x-pagination, model the success response, or stop the operation being detected as paginating. |
The spec-repo target
Raised when targets.spec_repo asks for a document your input cannot produce.
Both are refusals of an input domain, not of postponed work — and both stop the run rather than
publishing a spec repo with a document missing, because a published spec repo is a surface your
consumers read: an absent file there reads as a fact about your API rather than as a Glotto refusal.
| Code | Meaning |
|---|---|
GLOTTO_SPEC_REPO_INPUT_UNSUPPORTED |
The target is enabled for an input that has no spec document to publish. A GraphQL input is the case: it never becomes a canonical spec document, which is the same reason no spec changelog is produced for it. Remove spec_repo from targets:, or point the project at an OpenAPI or AsyncAPI spec. |
GLOTTO_SPEC_REPO_VARIANT_UNAVAILABLE |
One requested variant cannot be produced for this input; the message names which and why. with_code_samples needs openapi.code_samples.formats set (and is OpenAPI-only); with_transforms is OpenAPI-only, because the transform engine never runs on an AsyncAPI document. Drop that member from targets.spec_repo.variants — the remaining variants publish normally. Distinct from the code above so you can tell “your spec cannot be published” from “two of the three documents you asked for can”. |
Retiring a file Glotto no longer generates
Raised when a Glotto release stops emitting a file your project already has — a build config the stack outgrew, a module a newer layout replaced. Regeneration removes it, so your tree keeps matching the project Glotto builds today rather than accumulating files nothing reads.
Only a file Glotto can prove it wrote is ever removed, and only while your copy still matches
the bytes Glotto produced. Every generated source carries a @glotto:generated-checksum comment;
that comment is what makes the removal safe. A file with no checksum comment — anything you added
yourself, and anything Glotto emits unstamped, such as the machine-readable mirrors under
public/ — is left exactly where it is, whether or not Glotto still generates it. Turning a target
off in glotto.yml deletes nothing either: the tree it wrote stays until you remove it.
That leaves one case Glotto will not decide for you.
| Code | Meaning |
|---|---|
GLOTTO_RETIRED_FILE_EDITED |
Glotto no longer generates this file, and you have edited it since it was generated — so removing it would destroy work, and keeping it would leave your project carrying a file the current stack does not use. The run stops before writing anything, so your tree is untouched and every other file is exactly as you left it. Move whatever you still need into a file you own, then rerun. To retire it as-is, rerun with --force: Glotto copies the file to .glotto/backup/ before removing it and names the copy in its summary, so the bytes stay recoverable either way. |
Terraform objects with declared additional values
| Code | Target | When you see it | How to resolve it |
|---|---|---|---|
GLOTTO_TERRAFORM_DECLARED_OPEN_OBJECT_UNSUPPORTED |
Terraform | A resource or nested attribute object explicitly declares additional properties. Terraform generation would otherwise expose only the object’s named fields, losing those declared values. The message identifies the affected attribute path. | Generate an SDK or MCP target that preserves the declared additional values, or omit the Terraform target. Keep the schema’s declaration intact; removing it would change the API contract. |
Generation refuses this object before returning provider source instead of presenting a closed Terraform attribute schema as equivalent. Objects without the declaration and ordinary map attributes retain their existing behavior.