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

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

What Glotto emits — an OPEN union, so `archived` still decodes

typescript/src/models/message-role.ts MessageRole

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

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 · Python · Go · Ruby · Elixir — and the same section on every other Languages 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.