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

# Forward compatibility

APIs grow. A new enum member, a new response field — neither is a breaking change on the server,
and neither should break an SDK a consumer pinned last quarter. Generated SDKs are built so they
don't.

Three guarantees, all on by default, none requiring a regeneration to benefit from:

- **Open enums** — a value outside the set the SDK was generated from decodes, round-trips, and is
  detectable.
- **Unknown response fields** — a field the SDK was never generated from is retained on decode and
  written back out.
- **Per-call escape hatches** — you can send parameters your spec doesn't model yet.

## Open enums

When your server starts returning `status: "archived"` and the SDK was generated when the set was
`active | inactive`, a conventional generated enum throws on decode. Glotto's don't. An
unrecognized value satisfies three properties:

1. **It decodes without crashing.** No exception, no dropped response.
2. **It round-trips at its original value.** Read-modify-write sends `archived` back, not a
   fallback or a null.
3. **It is detectable.** You can tell a server-added value from a known member and branch on it.

This holds for **string** enums and, since Glotto took the deferred integer decision, for
**numeric** enums too — the same three-part contract over a different primitive.

In TypeScript that surfaces as an open union, so the known members still autocomplete while any
other value is accepted:

**A value the SDK predates, decoded intact**

Two enums out of one real spec, and one real generate run over it: a string enum and a numeric one, both emitted open. Neither type below is written on this page — a value outside either set decodes, round-trips, and compares unequal to every member the SDK knows.

Derived from `examples/realworld-assistant-api/inputs` — every byte below is sliced from that demo or from one real generate run over it.

**Your spec — a string enum with two members today** (`examples/realworld-assistant-api/inputs/assistant.openapi.yaml` `components → schemas → Message`)

```yaml
    Message:
      type: object
      required:
        - role
        - content
      properties:
        role:
          type: string
          enum:
            - user
            - assistant
        content:
          type: array
          items:
            $ref: '#/components/schemas/ContentBlock'
```

**Your spec — the same contract over a numeric enum** (`examples/realworld-assistant-api/inputs/assistant.openapi.yaml` `components → schemas → ErrorResponse`)

```yaml
    ErrorResponse:
      type: object
      description: The body every operation returns when a request fails.
      required:
        - status
        - message
      properties:
        status:
          type: integer
          description: The HTTP status the request was answered with.
          # A NUMERIC enum, open on the same three-part contract a string one is:
          # a status this SDK predates decodes, round-trips, and is detectable.
          enum:
            - 400
            - 401
            - 429
            - 500
        message:
          type: string
```

**glotto generate**

**What Glotto emits — an OPEN union, so `` `archived` `` still decodes** (`typescript/src/models/message-role.ts` `MessageRole`)

```ts
export type MessageRole = 'user' | 'assistant' | (string & {});
```

generated-checksum `4442693d5c56f9bfdd21804afa01eb02c8fe16c0f3f9d2813848d267301b81e1`

**What Glotto emits — the same shape over a number** (`typescript/src/models/error-response-status.ts` `ErrorResponseStatus`)

```ts
export type ErrorResponseStatus = 400 | 401 | 429 | 500 | (number & {});
```

generated-checksum `a6ee2afd7e6f9fe6638e48da045c76eae987bf808bc72132899fdf8f7eff6936`

Checking for a value this SDK predates is the comparison you would write anyway: it equals none of
the members above. Log it, route it, or ignore it — it survives re-encoding either way.

> The `& {}` is load-bearing, not noise. A plain `'active' | 'inactive' | string` collapses to
> `string` and discards the known members — which are the documentation of what the API actually
> accepts.

### How it surfaces in your language

Every engine keeps the same guarantee and spells it the way its language already handles
extensible values:

| Language | Surface | Detect an unrecognized value |
| --- | --- | --- |
| TypeScript, React Native | Open union (spelled out below) | It equals none of the generated members |
| Python | `StrEnum` / `IntEnum` with a `_missing_` hook returning a value-preserving pseudo-member | `.is_known` is `False` |
| Swift | A `RawRepresentable` struct wrapping the raw value, known members as static constants | `isKnown` is `false` |
| C#, Dart, Java, Kotlin | The same value-preserving wrapper shape, known members as constants | It equals no generated constant |
| Rust | A `#[non_exhaustive]` enum carrying the unrecognized value | Match its unknown variant |
| PHP | A union of the generated enum and its primitive (spelled out below) | The value is the raw primitive, not an enum case |
| Go, Ruby, Elixir | Raw-value passthrough — the declared type *is* the primitive | Compare against the generated constants |

Open unions are `'a' | 'b' | (string & {})` and `200 | 404 | (number & {})`.

For PHP, the exact surface is `Status|string`: the generated enum united with its primitive.

The passthrough row is not a weaker guarantee, just a different one: in those languages the
declared type never constrained the value in the first place, so an unrecognized member was
always going to arrive intact. The comparison you write is the same.

**Mixed and boolean enums stay closed** by design. A mixed enum has no single primitive to be open
over, and a boolean enum is already total — there is no value outside `true | false` to be
forward-compatible with.

## Unknown response fields

The second leg covers fields rather than values: a response field your SDK was never generated
from is retained on decode, readable through an accessor, and written back out on re-encode — so a
read-modify-write never silently deletes data the SDK didn't recognize.

Each language page documents the accessor its engine emits, in that language's idiom:
[TypeScript](/docs/typescript#unknown-response-fields) ·
[Python](/docs/python#unknown-response-fields) ·
[Go](/docs/go#unknown-response-fields) ·
[Ruby](/docs/ruby#unknown-response-fields) ·
[Elixir](/docs/elixir#unknown-response-fields) — and the same section on every other
[Languages](/docs/typescript) page.

## Sending what your spec doesn't model yet

The retained fields above are read-only by design: they record what the SDK *received*. To **send**
a parameter your spec doesn't model yet, use the per-call extra-query and extra-body escape hatches
rather than writing into the retained bag. That keeps the two directions honest — retention is a
record of the wire, and anything you add is something you chose to send.

## Why this matters more than it sounds

Forward compatibility is what makes a pinned SDK safe to leave pinned. Without it, every additive
server change is a silent deadline: consumers on older SDKs start throwing on values you consider
non-breaking, and you find out from their bug reports. With it, the additive change is exactly what
you intended it to be.
