# Forward compatibility

Forward compatibility ensures older SDK versions continue to work correctly when APIs evolve by adding new fields, enum values, or other data. This guide covers how Speakeasy SDKs handle these changes automatically and how to configure additional resilience.

## Quick reference

## How SDKs handle API changes

Speakeasy SDKs automatically handle common API evolution scenarios without requiring code changes.

### New fields

Adding new fields to API responses is always safe. SDKs ignore fields not defined in their model.

```yaml
type: object
properties:
  name:
    type: string
  created_at:
    type: string
    format: date-time
  updated_at: # New field - ignored by older SDKs
    type: string
    format: date-time
```

### New enum values

APIs often add new enum values over time. The `x-speakeasy-unknown-values` extension enables SDKs to handle unknown values gracefully instead of throwing errors.

```yaml
status:
  type: string
  x-speakeasy-unknown-values: allow
  enum:
    - active
    - inactive
    - pending
```

When the API adds a new value (e.g., `suspended`), each language handles it differently:

<CodeWithTabs client:visible
  tabs={[
    {
      label: "TypeScript",
      language: "typescript",
      code: `// Unknown values are captured in a type-safe wrapper
const status = response.status;
// Type: "active" | "inactive" | "pending" | Unrecognized<string>

// Check for unknown values
if (typeof status === "object") {
  console.log("Unknown status:", status.value);
}`,
    },
    {
      label: "Python",
      language: "python",
      code: `# Unknown values pass through as raw strings
status = response.status
## Type: Status | str

## Check for unknown values
if status not in Status:
    print(f"Unknown status: {status}")`,
    },
    {
      label: "Go",
      language: "go",
      code: `// Unknown values are accepted as the underlying type
status := response.Status
// Type: Status (string)

// Check for unknown values
if status != StatusActive && status != StatusInactive && status != StatusPending {
    fmt.Println("Unknown status:", status)
}`,
    },
    {
      label: "Java",
      language: "java",
      code: `// Unknown values are wrapped with isKnown() method
Status status = response.getStatus();

// Check for unknown values
if (!status.isKnown()) {
    System.out.println("Unknown status: " + status.value());
}`,
    },
    {
      label: "Ruby",
      language: "ruby",
      code: `# Open enums accept unknown values without raising
status = response.status
## Type: Shared::Status (open enum)

## Check for unknown values
unless status.known?
  puts "Unknown status: \\#{status.serialize}"
end`,
    },
  ]}
/>

### New response codes

Using status code ranges allows APIs to add specific codes without breaking SDKs:

```yaml
responses:
  "2xx":
    description: Success response
    content:
      application/json:
        schema:
          $ref: "#/components/schemas/SuccessResponse"
  "4xx":
    description: Error response
    content:
      application/json:
        schema:
          $ref: "#/components/schemas/ErrorResponse"
```

When an API returns an unexpected status code, SDKs match it to the appropriate range (`2xx`, `4xx`, `5xx`), parse the response using that range's schema, and provide access to both the status code and response body.

### Unexpected data

SDKs include additional mechanisms for handling unexpected data:

- **Validation errors**: Detailed error messages when unexpected data is received
- **OneOf schemas**: Attempts to match against known variants before failing
- **Optional fields**: Missing optional fields never cause validation errors

## Configuring SDK resilience

Generator options enable additional forward compatibility features. These are configured in `gen.yaml`.

### Forward-compatible enums

When `forwardCompatibleEnumsByDefault` is enabled (the default, except for Terraform), enums accept unknown values instead of rejecting the response:

```typescript
const notification = await sdk.notifications.get(id);
// Before: Error: Expected 'email' | 'sms' | 'push'
// After:  'email' | 'sms' | 'push' | Unrecognized<string>
```

### Forward-compatible unions

When `forwardCompatibleUnionsByDefault` is enabled (TypeScript, Python, Go, Java, Ruby), discriminated unions accept unknown variants:

```typescript
const account = await sdk.accounts.getLinkedAccount();
// Before: Error: Unable to deserialize into any union member
// After:
//   | { type: "email"; email: string }
//   | { type: "google"; googleId: string }
//   | { type: "UNKNOWN"; raw: unknown }
```

### Lax mode (TypeScript)

When `laxMode` is set to `lax` (the default for new TypeScript SDKs), the SDK handles missing or mistyped fields by applying zero-value defaults and type coercions:

- Missing required strings become `""`
- Missing required numbers become `0`
- Missing required booleans become `false`
- Missing required dates become `Date(0)` (Unix epoch)
- Missing required bigints become `0n`
- Missing required literals become the literal value
- String `"true"` and `"false"` are coerced to booleans
- Numeric strings are coerced to numbers
- Numbers are coerced to dates (treated as milliseconds)
- Strings are coerced to bigints
- Any non-string value is coerced to string via `JSON.stringify()`

Lax mode only affects response deserialization and never lies about types.

<Callout type="info" title="Go zero-value defaults">
Go SDKs can achieve similar resilience with `respectRequiredFields: false`, which applies zero-value defaults for missing required fields. However, Go does not perform type coercions like TypeScript's lax mode.
</Callout>

### Smart union deserialization

When `unionStrategy` is set to `populated-fields` (the default for new TypeScript, Go, and Ruby SDKs), the SDK picks the best union variant by trying all types and returning the one with the most matching fields. When there's a tie, it picks the variant with the fewest coerced or inexact fields.

This prevents issues where one union variant is a subset of another and the wrong variant gets selected due to ordering.

<Callout type="info" title="Per-schema overrides">
Individual enums and unions can override global defaults using `x-speakeasy-unknown-values: allow` or `x-speakeasy-unknown-values: disallow` in the OpenAPI spec. See the [TypeScript configuration reference](/docs/speakeasy-reference/generation/ts-config#forward-compatibility), [Ruby configuration reference](/docs/speakeasy-reference/generation/ruby-config#forward-compatibility), or [Terraform configuration reference](/docs/speakeasy-reference/generation/terraform-config#forward-compatibility) for all available options.
</Callout>

## Evolving APIs safely

Best practices for making changes without breaking existing SDK users.

### Deprecating fields

Mark fields as deprecated before removing them:

```yaml
properties:
  name:
    type: string
  sku:
    type: string
    deprecated: true
    x-speakeasy-deprecation-message: We no longer support the SKU property.
```

This keeps fields accessible to older SDKs while new SDKs show deprecation warnings. When removing a field entirely:

1. Mark the field as optional first
2. Add deprecation notices
3. Allow time for users to update
4. Remove the field after a suitable deprecation period

### Forward-compatible union patterns

To create unions that handle future data types, use the oneOf pattern with a string fallback:

```yaml
oneOf:
  - { type: "dog" }
  - { type: "cat" }
  - { type: string }
```

This provides strongly typed handling for known variants while gracefully capturing future variants as strings.

### Versioning strategies

Common approaches to manage breaking changes:

- **Path-based versioning**: `/v1/resource`, `/v2/resource`
- **Header-based versioning**: `Api-Version: 2023-01-01`
- **Multiple versions**: Maintain multiple API versions during migration periods

### Adding defaults

When making required fields optional, include default values:

```yaml
properties:
  status:
    type: string
    default: "active"
```

### Detecting breaking changes

The [OpenAPI diff tool](/docs/speakeasy-reference/cli/openapi/diff) identifies potential breaking changes:

```bash
speakeasy openapi diff --base v1.yaml --revision v2.yaml
```

This highlights changes like removing required fields, changing field types, or modifying oneOf schemas.

### Automatic SDK versioning

Speakeasy manages SDK versions based on the nature of changes:

- **Patch**: Non-breaking changes
- **Minor**: Backward-compatible additions
- **Major**: Breaking changes

When generating SDKs, Speakeasy detects breaking changes and provides clear notifications about what changed and how to handle the transition.

<Callout type="info" title="Related resources">
  For more information about handling breaking changes, see the [breaking
  changes guide](./breaking-changes).
</Callout>
