Skip to content

refactor: open enums with lax client - #2760

Draft
mariechatfield wants to merge 4 commits into
refactor/marie/SDK-1307-rc4from
refactor/marie/SDK-1307-rc3
Draft

refactor: open enums with lax client#2760
mariechatfield wants to merge 4 commits into
refactor/marie/SDK-1307-rc4from
refactor/marie/SDK-1307-rc3

Conversation

@mariechatfield

@mariechatfield mariechatfield commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Stacked on #2759 (which installs rc.4 -- lax enums/unions, strict dates). This branch swaps that for 0.3.0-rc.3, which is lax on dates too, and adds only what that costs: the RFCDate->Date migration.

Base is refactor/marie/SDK-1307-rc4, not main, on purpose -- the diff here is deliberately just the incremental cost of choosing rc.3 over rc.4:

  • 63a24fbe -- bump to rc.3.
  • 29d922ac -- the RFCDate->Date migration this reintroduces (22 files). Includes one inbound-parsing gotcha worth flagging in review: reading a wire date back with local Date components (.toString(), new Date(str)) rather than through formatWireDateToStringDate silently shifts the day in some timezones -- see that helper's TSDoc in src/helpers/dateFormatting.ts.

Everything else (open-enum narrowing, the discriminated-union guard, the exhaustiveness fallback) is identical to #2759 and not repeated here -- review those there.

Why this exists

#2759's description frames the decision as rc.4-strict-dates vs. rc.3-lax-dates. This PR makes the rc.3 side of that comparison concrete: it's the RFCDate migration, and nothing else. If the team decides rc.4 is the way to go, this branch/PR can just be closed without merging.

A concrete case for why dates and null-tolerance aren't separable

Observed a live example: PUT .../employees/:id/pay_schedule can return transition_pay_period: null, even though the OAS schema says that field is either a full object or absent -- never null. That's the same crash class SDK-1307 exists to fix, just a different shape (a structural null, not an unrecognized enum/union member).

Checked both packages directly to see whether rc.4 (strict dates) still catches this:

  • rc.3 (laxMode: lax): the field's generated schema is types.optional(...), and the SDK's own types.optional() helper (src/types/primitives.ts) unions in a z.null().transform(() => undefined) branch. A literal null parses cleanly to undefined.
  • rc.4 (laxMode: strict): same field, plain Zod .optional() -- tolerates a missing key, not an explicit null. This throws ResponseValidationError, uncaught, same as before any of this work started.

laxMode is a single gen.yaml flag (lax/strict), unlike forwardCompatibleEnumsByDefault/forwardCompatibleUnionsByDefault, which are separate flags -- that's specifically why rc.4 can keep lenient enums/unions while reverting to strict dates. There's no equivalent scoped flag for "tolerate null on an optional-but-never-null field" independent of dates -- confirmed by diffing the two packages' generated code, not just going on the changelog description. If we want this class of null-handling crash covered by the generator (rather than defensively coded per call site), that requires laxMode: lax -- i.e. rc.3, with the RFCDate migration in this PR as the cost.

Worth noting for scope: our SDK code shows zero type changes either way, because the exported TS type (transitionPayPeriod?: PayScheduleAssignmentTransitionPayPeriod | undefined) is byte-identical between rc.3 and rc.4 -- neither ever declared | null. The leniency is a runtime-only difference, invisible to tsc, so tsc clean on rc.4 does not mean rc.4 is safe from this crash.

Test plan

  • tsc --noEmit clean (0 errors) on this branch against rc.3
  • Full test suite green except the same pre-existing timing flakes as refactor: open enums with strict client #2759 (PayrollConfiguration.test.tsx gross-up, PaymentFlow.test.tsx, PayrollLanding.test.tsx -- all pass in isolation)
  • eslint clean (only pre-existing, unrelated warnings)

🤖 Generated with Claude Code

mariechatfield and others added 3 commits September 11, 2026 14:25
Same lax-mode enum/union parsing as rc.4, but rc.3 also relaxes date
parsing, which reintroduces the RFCDate->Date migration the rc.4
branch (#2759) doesn't need. This branch exists to compare that cost
against rc.4's strict-dates variant.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lax mode drops the RFCDate wrapper on date-only fields in favor of plain
Date, parsed off the wire at UTC midnight (Speakeasy's types.date()).
Outbound sites just need the Date passed through instead of wrapped.
Inbound sites are the real risk: several read the API's Date back out
via .toString() (safe under the old RFCDate, which always round-tripped
through the same UTC frame) or local Date components, both of which
silently roll the calendar day back by one west of UTC. Fixed with a new
formatWireDateToStringDate helper (dateFormatting.ts) for the UTC-safe
read, alongside the existing local-midnight helpers for outbound use.

Found and fixed real latent bugs beyond the mechanical RFCDate swap:
getPendingFutureHomeAddress's sort/filter and several HomeAddressView
display sites were comparing/parsing Date.toString() output, not a
YYYY-MM-DD string, once the field type changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mariechatfield mariechatfield changed the title SDK-1307: rc.3 variant -- what strict-vs-lax dates cost beyond rc.4 (#2759) refactor: open enums with lax client Sep 11, 2026
Drift from the RFCDate->Date migration that the pre-commit hook
missed (it only formats staged files at commit time) but CI's
format:check catches.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant