SDKs
Upgrade guide: TypeScript forward compatibility and fault tolerance
David Adler
December 1, 2025 - 5 min read
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-fieldsEach of these settings addresses a specific class of API evolution issues. The sections below explain what each setting does and how to configure it.
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: trueWhat 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-onlyWhat 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 unionsLax 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 behaviorWhat 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 Type | Missing Value | Default |
|---|---|---|
| Required string | null or undefined | "" |
| Required number | null or undefined | 0 |
| Required boolean | null or undefined | false |
| Required date | null or undefined | Date(0) (Unix epoch) |
| Required literal | null or undefined | The literal/const value |
| Required bigint | null or undefined | BigInt(0) |
For nullable and optional fields, lax mode handles the common confusion between null and undefined:
- Nullable field that received
undefinedis coerced tonull - Optional field that received
nullis coerced toundefined
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-fieldsWhat 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-fieldsAfter 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.