Speakeasy Logo
Skip to Content

SDKs

Upgrade guide: TypeScript forward compatibility and fault tolerance

David Adler

David Adler

December 1, 2025 - 5 min read

SDKs

Speakeasy TypeScript SDKs now support forward compatibility features that let your SDK gracefully handle API evolution. When you add a new enum value, extend a union type, or adjust response fields, SDK users on older versions continue working without errors.

This guide walks through how to enable these features in your existing SDK configuration.

What’s changing

New Speakeasy TypeScript SDKs now include forward compatibility features enabled by default. If you’re upgrading an existing SDK, you’ll need to opt in to these features in your gen.yaml configuration.

Here’s a quick reference of the settings to add:

typescript: forwardCompatibleEnumsByDefault: true forwardCompatibleUnionsByDefault: tagged-only laxMode: lax unionStrategy: populated-fields

Each of these settings addresses a specific class of API evolution issues. The sections below explain what each setting does and how to configure it.

  1. Forward-compatible enums
  2. Forward-compatible unions
  3. Lax mode
  4. Smart union deserialization

Forward-compatible enums

Enums are one of the most common sources of breaking changes. When your API adds a new status, category, or type value, SDKs with strict enum validation will reject the entire response—even though the request succeeded.

To enable forward-compatible enums, add this to your gen.yaml:

typescript: forwardCompatibleEnumsByDefault: true

What this changes: Any enum used in a response will automatically accept unknown values. Single-value enums won’t be automatically opened. For finer control, use the x-speakeasy-unknown-values: allow or x-speakeasy-unknown-values: disallow extension on individual enums in your OpenAPI spec.

Example scenario: Your notification system starts with email and sms, then you add push notifications. Without forward compatibility, SDK users on older versions see errors until they upgrade. With forward compatibility enabled, they receive the value gracefully:

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

When SDK users receive a known value, everything works exactly as before. Unknown values are captured in a type-safe Unrecognized wrapper.

Forward-compatible unions

Similar to enums, discriminated unions often get extended as your API evolves. A linked account system might start with email and Google authentication, then you add Apple or GitHub options later.

To enable forward-compatible unions, add this to your gen.yaml:

typescript: forwardCompatibleUnionsByDefault: tagged-only

What this changes: Tagged unions will automatically accept unknown discriminator values. For finer control, use x-speakeasy-unknown-values: allow or x-speakeasy-unknown-values: disallow on specific unions in your OpenAPI spec.

Example scenario: You add GitHub as a new linked account type. SDK users on older versions won’t crash—they’ll receive an UNKNOWN variant with access to the raw response data:

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"; rawValue: unknown } // Automatically inserted for open unions

Lax mode

Sometimes the issue isn’t new values being added, but missing or mistyped fields. A single missing required field can cause the entire response to fail deserialization, even when all the data SDK users actually need is present.

To enable lax mode, add this to your gen.yaml:

typescript: laxMode: lax # or 'strict' to keep current behavior

What this changes: Lax mode applies sensible defaults when fields are missing or have minor type mismatches. It only kicks in when the payload doesn’t quite match the schema—correctly documented APIs see no change in behavior.

Why this matters: The SDK types never lie to your users. Lax mode fills in sensible defaults rather than returning incorrect data, applying only non-lossy coercions. This approach is inspired by Go’s built-in JSON unmarshal behavior and Pydantic’s coercion tables.

For required fields that are missing, lax mode fills in zero values:

Field TypeMissing ValueDefault
Required stringnull or undefined""
Required numbernull or undefined0
Required booleannull or undefinedfalse
Required datenull or undefinedDate(0) (Unix epoch)
Required literalnull or undefinedThe literal/const value
Required bigintnull or undefinedBigInt(0)

For nullable and optional fields, lax mode handles the common confusion between null and undefined:

  • Nullable field that received undefined is coerced to null
  • Optional field that received null is coerced to undefined

Lax mode also provides fallback coercion for type mismatches:

  • Required string: any value coerced to string with JSON.stringify()
  • Required boolean: coerces the strings "true" and "false"
  • Required number: attempts to coerce strings to valid numbers
  • Required date: attempts to coerce strings to dates, coerces numbers (in milliseconds) to dates
  • Required bigint: attempts to coerce strings to bigints

Smart union deserialization

When deserializing union types, the order of options matters. The default left-to-right strategy tries each type in order and returns the first valid match. This works well when types have distinct required fields, but can pick the wrong option when one type is a subset of another.

To enable smart union deserialization, add this to your gen.yaml:

typescript: unionStrategy: populated-fields

What this changes: Instead of picking the first valid match, the SDK tries all union options and returns the one with the most matching fields (including optional fields). This approach is inspired by Pydantic’s union deserialization algorithm.

How it works: The algorithm attempts to deserialize into all union options and rejects any that fail validation. It picks the candidate with the most populated fields. If there’s a tie, it picks the candidate with the fewest “inexact” fields (where coercion happened, such as an open enum accepting an unknown value).

Example scenario: You have a union of BasicUser and AdminUser where AdminUser has all the fields of BasicUser plus additional admin-specific fields. The populated-fields strategy correctly identifies admin users even if BasicUser is listed first in the union.

Complete upgrade checklist

To upgrade your existing TypeScript SDK with all forward compatibility features, add these settings to your gen.yaml:

typescript: forwardCompatibleEnumsByDefault: true forwardCompatibleUnionsByDefault: tagged-only laxMode: lax unionStrategy: populated-fields

After updating your configuration, regenerate your SDK with speakeasy run. The changes are backward compatible—your SDK users won’t need to change their code, but they’ll benefit from improved resilience to API evolution.

What to expect

These features work together to provide a robust experience for your SDK users:

  • Forward-compatible enums and unions handle additive changes to your API
  • Lax mode handles missing or mistyped fields gracefully
  • Smart union deserialization handles ambiguous type discrimination

All without sacrificing the type safety and developer experience that make TypeScript SDKs valuable.

If you run into any issues during the upgrade or have questions about specific scenarios, reach out to the Speakeasy team.

Note: This post focuses on TypeScript SDK behavior. Speakeasy SDKs for other languages (Python, Go, Java, and more) implement similar forward compatibility behaviors tailored to each language’s idioms.

Last updated on

Build with
confidence.

Ship what's next.