Skip to content

fix: Omit undefined optional fields from deserialised output#1488

Merged
gjtorikian merged 4 commits intoworkos:mainfrom
smorimoto:fix-optional-field-semantics-in-deserializers
Feb 19, 2026
Merged

fix: Omit undefined optional fields from deserialised output#1488
gjtorikian merged 4 commits intoworkos:mainfrom
smorimoto:fix-optional-field-semantics-in-deserializers

Conversation

@smorimoto
Copy link
Contributor

@smorimoto smorimoto commented Feb 13, 2026

Summary

  • Switches all deserialisers to the ...(value !== undefined && { key }) spread-guard pattern so that optional fields absent from an API response are omitted entirely rather than included as undefined
  • Preserves explicit null values (e.g. verificationToken: null) which are semantically meaningful
  • Updates the List / ListResponse pagination cursor types from string to string | null to match the values the API actually returns
  • Updates PaginationOptions and ListWarrantsOptions to accept string | null for before/after, so that pagination cursors from API responses can be passed straight through without conversion
  • Updates affected unit tests and snapshots accordingly

Affected deserialisers

File Fields
session.serializer.ts organizationId, impersonator
connection.serializer.ts organizationId
authentication-event.serializer.ts error
profile.serializer.ts organizationId, firstName, lastName, role, roles, groups, customAttributes, rawAttributes
directory-user.serializer.ts role, roles (both deserializeDirectoryUser and deserializeUpdatedEventDirectoryUser)
organization-domain.serializer.ts verificationToken
vault-object.serializer.ts value, listMetadata.after, listMetadata.before

Pagination cursor type changes

Interface Field Before After
List.listMetadata before, after string string | null
ListResponse.list_metadata before, after string string | null
PaginationOptions before, after string string | null
ListWarrantsOptions after string string | null
SerializedListWarrantsOptions after string string | null

Discussion: Pagination cursor type (string vs string | null)

The WorkOS API documentation defines the after and before pagination cursors as type string, yet the response examples clearly return null when there is no further page:

{
  "data": [...],
  "list_metadata": {
    "after": null,
    "before": "object_id_xxx"
  }
}

For example, see the Vault list objects reference.

This PR updates both the response types (List, ListResponse) and the input types (PaginationOptions, ListWarrantsOptions) to string | null. This means pagination cursors from API responses can be passed directly back into list options without any coercion (e.g. ?? undefined), making the API ergonomic and type-honest.

If the documentation is authoritative and the API should be returning undefined (i.e. omitting the key) instead of null, then a server-side change may be needed. Either way, the SDK types should match the actual response shape, and the input types should accept whatever the output types produce.

Test plan

  • npx tsc --noEmit passes
  • All 590 tests pass (31 suites)
  • All 17 snapshots pass
  • Prettier and ESLint clean

@smorimoto smorimoto requested a review from a team as a code owner February 13, 2026 18:43
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 13, 2026

Greptile Overview

Greptile Summary

This PR improves the SDK's type safety by ensuring that optional fields absent from API responses are omitted from deserialized objects rather than explicitly set to undefined. The changes apply the spread-guard pattern ...(value !== undefined && { key }) consistently across all serializers, which provides a cleaner API surface and better aligns with TypeScript best practices.

Key improvements:

  • Pagination cursor types updated from string to string | null in List and ListResponse interfaces to match actual API behavior
  • Optional fields in serializers now omitted when undefined rather than explicitly included
  • Explicit null values (like verificationToken: null) are preserved as they carry semantic meaning
  • All affected tests and snapshots updated to reflect the new behavior
  • Pagination logic in live tests updated to handle the new nullable cursor types correctly

The implementation is consistent, well-tested, and maintains backward compatibility for consumers who check for field presence using in operator or hasOwnProperty instead of checking for undefined values.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The changes are purely about data representation consistency - converting undefined fields to omitted fields. The pattern is applied systematically across all serializers, all tests pass, and the approach aligns with TypeScript best practices. The type changes for pagination cursors correctly reflect the actual API response shape.
  • No files require special attention

Important Files Changed

Filename Overview
src/common/interfaces/list.interface.ts Updated pagination cursor types from string to `string
src/sso/serializers/profile.serializer.ts Applied spread-guard pattern to omit undefined optional fields (organizationId, firstName, lastName, role, roles, groups, customAttributes, rawAttributes) instead of including them as undefined
src/user-management/serializers/session.serializer.ts Applied spread-guard pattern to organizationId and impersonator fields to omit them when undefined
src/directory-sync/serializers/directory-user.serializer.ts Applied spread-guard pattern to role and roles fields in both deserializeDirectoryUser and deserializeUpdatedEventDirectoryUser functions
src/vault/serializers/vault-object.serializer.ts Applied spread-guard pattern to value field in deserializeObject and to after/before pagination cursors in deserializeListObjects, replacing the previous ?? undefined approach

Sequence Diagram

sequenceDiagram
    participant API as WorkOS API
    participant SDK as SDK Client
    participant Serializer as Deserializer
    participant App as Application

    API->>SDK: API Response (snake_case)<br/>{organization_id: undefined}
    SDK->>Serializer: deserializeProfile(response)
    
    alt Before this PR
        Serializer->>Serializer: organizationId: response.organization_id
        Serializer->>App: {organizationId: undefined, ...}
        Note over App: Field present but undefined
    end
    
    alt After this PR
        Serializer->>Serializer: ...(response.organization_id !== undefined && {organizationId})
        Serializer->>App: {id, email, ...}<br/>(organizationId omitted)
        Note over App: Field not present in object
    end
    
    API->>SDK: Pagination Response<br/>{after: null, before: "cursor"}
    SDK->>Serializer: deserializeListObjects(response)
    
    alt After this PR
        Serializer->>Serializer: Type: string | null
        Serializer->>App: {listMetadata: {after: null, before: "cursor"}}
        Note over App: null preserved (semantic meaning)
    end
Loading

Last reviewed commit: 862bedc

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

12 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@smorimoto smorimoto force-pushed the fix-optional-field-semantics-in-deserializers branch from 862bedc to ca081aa Compare February 13, 2026 18:53
…cluding them as undefined

When an optional field is not present in an API response, the
deserialiser should not include the key at all rather than setting
it to `undefined`.  This change switches every affected deserialiser
to the spread-guard pattern `...(value !== undefined && { key })`,
which preserves explicit `null` values while omitting truly absent
keys.

Also updates the `List` / `ListResponse` pagination cursor types
from `string` to `string | null` so that the `null` values the API
actually returns are represented faithfully.
@smorimoto smorimoto force-pushed the fix-optional-field-semantics-in-deserializers branch 2 times, most recently from 6d33451 to 656a48f Compare February 13, 2026 19:15
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 656a48f0c5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

HttpClient.getQueryString previously only stripped empty strings and
undefined, allowing null to be serialised as the literal query value
"null".  This fixes getQueryString to also reject null, and updates
PaginationOptions and ListWarrantsOptions to accept string | null so
that API response cursors can be passed straight through.
@smorimoto smorimoto force-pushed the fix-optional-field-semantics-in-deserializers branch from 656a48f to 1a510e7 Compare February 13, 2026 19:27
@gjtorikian gjtorikian changed the title Omit undefined optional fields from deserialised output fix: Omit undefined optional fields from deserialised output Feb 19, 2026
@gjtorikian
Copy link
Contributor

looks good, thank you!

@gjtorikian gjtorikian merged commit 46c9765 into workos:main Feb 19, 2026
7 checks passed
@smorimoto smorimoto deleted the fix-optional-field-semantics-in-deserializers branch February 19, 2026 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments