[typescript-fetch] centralise date handling, add dateLibrary, fix for… - #24637
[typescript-fetch] centralise date handling, add dateLibrary, fix for…#24637b2l wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
2 issues found across 278 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="samples/client/petstore/typescript-fetch/builds/date-library-string/docs/DefaultApi.md">
<violation number="1" location="samples/client/petstore/typescript-fetch/builds/date-library-string/docs/DefaultApi.md:33">
P3: The example code block uses unquoted date literals (`startsOn: 2013-10-20`) for parameters typed as `string`. Copying this example fails to compile — unquoted `2013-10-20` is parsed as arithmetic `2013 - 10 - 20` (a number that violates the `string` type) and `2013-10-20T19:20:30+01:00` is invalid syntax. Quote the values as string literals in the generated example.</violation>
</file>
<file name="modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache:339">
P3: In the centralized date-handling design, `querystringSingleKey` still serializes any `Date` value it receives with `serializeDateTime` (a full UTC instant). This means a `format: date` value that reaches the querystring leaf directly (e.g. nested/array query values, or a Date passed for a date primitive that bypasses `apisAssignQueryParam`) is rendered as a timestamp instead of a local-calendar date, diverging from the new `serializeDate` path used by the query/form/path assignments. Since `querystringSingleKey` can't tell a date from a date-time, consider documenting this limitation or having date primitives pre-serialized before reaching this leaf so the local-date semantics stay consistent.</violation>
</file>
Note: This PR contains a large number of files. cubic only reviews up to 200 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
Re-trigger cubic
|
|
||
| const body = { | ||
| // string | ||
| startsOn: 2013-10-20, |
There was a problem hiding this comment.
P3: The example code block uses unquoted date literals (startsOn: 2013-10-20) for parameters typed as string. Copying this example fails to compile — unquoted 2013-10-20 is parsed as arithmetic 2013 - 10 - 20 (a number that violates the string type) and 2013-10-20T19:20:30+01:00 is invalid syntax. Quote the values as string literals in the generated example.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/typescript-fetch/builds/date-library-string/docs/DefaultApi.md, line 33:
<comment>The example code block uses unquoted date literals (`startsOn: 2013-10-20`) for parameters typed as `string`. Copying this example fails to compile — unquoted `2013-10-20` is parsed as arithmetic `2013 - 10 - 20` (a number that violates the `string` type) and `2013-10-20T19:20:30+01:00` is invalid syntax. Quote the values as string literals in the generated example.</comment>
<file context>
@@ -0,0 +1,149 @@
+
+ const body = {
+ // string
+ startsOn: 2013-10-20,
+ // string (optional)
+ createdAt: 2013-10-20T19:20:30+01:00,
</file context>
| } | ||
| if (value instanceof Date) { | ||
| return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; | ||
| return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; |
There was a problem hiding this comment.
P3: In the centralized date-handling design, querystringSingleKey still serializes any Date value it receives with serializeDateTime (a full UTC instant). This means a format: date value that reaches the querystring leaf directly (e.g. nested/array query values, or a Date passed for a date primitive that bypasses apisAssignQueryParam) is rendered as a timestamp instead of a local-calendar date, diverging from the new serializeDate path used by the query/form/path assignments. Since querystringSingleKey can't tell a date from a date-time, consider documenting this limitation or having date primitives pre-serialized before reaching this leaf so the local-date semantics stay consistent.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache, line 339:
<comment>In the centralized date-handling design, `querystringSingleKey` still serializes any `Date` value it receives with `serializeDateTime` (a full UTC instant). This means a `format: date` value that reaches the querystring leaf directly (e.g. nested/array query values, or a Date passed for a date primitive that bypasses `apisAssignQueryParam`) is rendered as a timestamp instead of a local-calendar date, diverging from the new `serializeDate` path used by the query/form/path assignments. Since `querystringSingleKey` can't tell a date from a date-time, consider documenting this limitation or having date primitives pre-serialized before reaching this leaf so the local-date semantics stay consistent.</comment>
<file context>
@@ -336,7 +336,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef
}
if (value instanceof Date) {
- return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`;
+ return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`;
}
if (value instanceof Object) {
</file context>
There was a problem hiding this comment.
14 issues found across 228 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="samples/client/petstore/typescript-fetch/builds/default-v3.0/runtime.ts">
<violation number="1" location="samples/client/petstore/typescript-fetch/builds/default-v3.0/runtime.ts:378">
P2: Dates outside the four-digit year range are silently changed during serialization because `slice(-4)` truncates the year; validating the year range before padding would avoid sending a different calendar date.</violation>
</file>
<file name="samples/client/petstore/typescript-fetch/builds/enum/runtime.ts">
<violation number="1" location="samples/client/petstore/typescript-fetch/builds/enum/runtime.ts:378">
P2: Dates outside the four-digit year range are serialized as the wrong calendar year because `slice(-4)` drops leading digits; reject years below 0 or above 9999 before formatting so a valid `Date` cannot silently become a different server value.</violation>
</file>
<file name="samples/client/petstore/typescript-fetch/builds/oneOf/runtime.ts">
<violation number="1" location="samples/client/petstore/typescript-fetch/builds/oneOf/runtime.ts:378">
P2: `serializeDate` silently changes dates whose local year is outside `0000`–`9999`: `slice(-4)` turns year 10000 into `0000` (and negative years into malformed text). Reject years outside the four-digit `format: date` range before truncating instead of emitting a different calendar date.</violation>
</file>
<file name="samples/client/petstore/typescript-fetch/builds/date-library-date/runtime.ts">
<violation number="1" location="samples/client/petstore/typescript-fetch/builds/date-library-date/runtime.ts:378">
P2: Dates with years outside the four-digit OpenAPI range are silently serialized to the wrong calendar year because `slice(-4)` truncates the year (year 10000 becomes 0000). Reject unsupported years before formatting rather than sending a different date.</violation>
</file>
<file name="samples/client/others/typescript-fetch/multipart-file-array/runtime.ts">
<violation number="1" location="samples/client/others/typescript-fetch/multipart-file-array/runtime.ts:378">
P2: Dates outside the four-digit year range are silently serialized as a different date because `.slice(-4)` truncates the year; for example, year 10000 becomes `0000`. Reject years below 0 or above 9999 before padding so request and model data cannot be corrupted.</violation>
</file>
<file name="samples/client/others/typescript-fetch/infinite-recursion-issue/runtime.ts">
<violation number="1" location="samples/client/others/typescript-fetch/infinite-recursion-issue/runtime.ts:378">
P2: Native `Date` values outside years 0000–9999 are serialized to the wrong year because this ES6 padding expression truncates rather than pads. Validating the four-digit RFC3339 year before formatting would prevent silent date corruption.</violation>
</file>
<file name="samples/client/petstore/typescript-fetch/builds/allOf-nullable/runtime.ts">
<violation number="1" location="samples/client/petstore/typescript-fetch/builds/allOf-nullable/runtime.ts:378">
P2: A `Date` outside the four-digit calendar range is silently sent as a different date because `slice(-4)` truncates years above 9999 and does not handle negative years; validating the year range before formatting would prevent incorrect requests.</violation>
</file>
<file name="samples/client/petstore/typescript-fetch/builds/prefix-parameter-interfaces/src/runtime.ts">
<violation number="1" location="samples/client/petstore/typescript-fetch/builds/prefix-parameter-interfaces/src/runtime.ts:378">
P2: A valid `Date` outside the four-digit range is silently serialized as a different year because `slice(-4)` truncates long or negative year strings. Preserve the full year or reject years outside the RFC3339 four-digit range before formatting so the client cannot send a corrupted calendar date.</violation>
</file>
<file name="samples/client/petstore/typescript-fetch/builds/multiple-parameters/runtime.ts">
<violation number="1" location="samples/client/petstore/typescript-fetch/builds/multiple-parameters/runtime.ts:378">
P2: A native `Date` outside the four-digit year range is serialized with a different year, corrupting date path, query, form, or model values. Validating the numeric year before the ES6-compatible padding prevents this silent data change.</violation>
</file>
<file name="samples/client/petstore/typescript-fetch/builds/es6-target/src/runtime.ts">
<violation number="1" location="samples/client/petstore/typescript-fetch/builds/es6-target/src/runtime.ts:378">
P2: `serializeDate` silently wraps years greater than 9999 to year 0000 and produces malformed output for negative years, so a caller-provided `Date` can be serialized as a different calendar date. Validate the local year is within the four-digit range before applying the suffix slice and throw instead of truncating it.</violation>
</file>
<file name="samples/client/petstore/typescript-fetch/builds/sagas-and-records/src/runtime.ts">
<violation number="1" location="samples/client/petstore/typescript-fetch/builds/sagas-and-records/src/runtime.ts:378">
P2: Dates outside the four-digit `format: date` year range are silently truncated or malformed by this padding expression, so valid native `Date` inputs can be sent as a different calendar date. Checking that the year is between 0 and 9999 before applying the ES6-compatible padding would prevent silent data corruption.</violation>
</file>
<file name="modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache:369">
P2: A `Date` outside the four-digit year range is sent with a different year because `slice(-4)` truncates extended or negative years; rejecting years outside `0000–9999` before padding would avoid corrupting the request.</violation>
<violation number="2" location="modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache:394">
P2: Malformed calendar dates still pass the generated oneOf date guard and become `Invalid Date`; aligning the oneOf guard with `parseDate` would prevent invalid date values from being selected as a valid branch.</violation>
</file>
<file name="samples/client/others/typescript-fetch/self-import-issue/runtime.ts">
<violation number="1" location="samples/client/others/typescript-fetch/self-import-issue/runtime.ts:402">
P2: On time zones that skip only local midnight, `parseDate` returns 01:00 instead of local midnight because the validation ignores the hour. The validation could also require `date.getHours() === 0` and return `Invalid Date` when the requested midnight is not representable.</violation>
</file>
Note: This PR contains a large number of files. cubic only reviews up to 200 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
Re-trigger cubic
| throw new RangeError('Invalid time value'); | ||
| } | ||
| // Not padStart: the generated client may target ES6, where it does not exist. | ||
| const year = ('000' + value.getFullYear()).slice(-4); |
There was a problem hiding this comment.
P2: Dates outside the four-digit year range are silently changed during serialization because slice(-4) truncates the year; validating the year range before padding would avoid sending a different calendar date.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/typescript-fetch/builds/default-v3.0/runtime.ts, line 378:
<comment>Dates outside the four-digit year range are silently changed during serialization because `slice(-4)` truncates the year; validating the year range before padding would avoid sending a different calendar date.</comment>
<file context>
@@ -371,9 +371,13 @@ export function serializeDateTime(value: Date): string {
+ throw new RangeError('Invalid time value');
+ }
+ // Not padStart: the generated client may target ES6, where it does not exist.
+ const year = ('000' + value.getFullYear()).slice(-4);
+ const month = ('0' + (value.getMonth() + 1)).slice(-2);
+ const day = ('0' + value.getDate()).slice(-2);
</file context>
| const year = ('000' + value.getFullYear()).slice(-4); | |
| const yearValue = value.getFullYear(); | |
| if (yearValue < 0 || yearValue > 9999) { | |
| throw new RangeError('Invalid time value'); | |
| } | |
| const year = ('000' + yearValue).slice(-4); |
| throw new RangeError('Invalid time value'); | ||
| } | ||
| // Not padStart: the generated client may target ES6, where it does not exist. | ||
| const year = ('000' + value.getFullYear()).slice(-4); |
There was a problem hiding this comment.
P2: Dates outside the four-digit year range are serialized as the wrong calendar year because slice(-4) drops leading digits; reject years below 0 or above 9999 before formatting so a valid Date cannot silently become a different server value.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/typescript-fetch/builds/enum/runtime.ts, line 378:
<comment>Dates outside the four-digit year range are serialized as the wrong calendar year because `slice(-4)` drops leading digits; reject years below 0 or above 9999 before formatting so a valid `Date` cannot silently become a different server value.</comment>
<file context>
@@ -371,9 +371,13 @@ export function serializeDateTime(value: Date): string {
+ throw new RangeError('Invalid time value');
+ }
+ // Not padStart: the generated client may target ES6, where it does not exist.
+ const year = ('000' + value.getFullYear()).slice(-4);
+ const month = ('0' + (value.getMonth() + 1)).slice(-2);
+ const day = ('0' + value.getDate()).slice(-2);
</file context>
| const year = ('000' + value.getFullYear()).slice(-4); | |
| const fullYear = value.getFullYear(); | |
| if (fullYear < 0 || fullYear > 9999) { | |
| throw new RangeError('Invalid time value'); | |
| } | |
| const year = ('0000' + fullYear).slice(-4); |
| throw new RangeError('Invalid time value'); | ||
| } | ||
| // Not padStart: the generated client may target ES6, where it does not exist. | ||
| const year = ('000' + value.getFullYear()).slice(-4); |
There was a problem hiding this comment.
P2: serializeDate silently changes dates whose local year is outside 0000–9999: slice(-4) turns year 10000 into 0000 (and negative years into malformed text). Reject years outside the four-digit format: date range before truncating instead of emitting a different calendar date.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/typescript-fetch/builds/oneOf/runtime.ts, line 378:
<comment>`serializeDate` silently changes dates whose local year is outside `0000`–`9999`: `slice(-4)` turns year 10000 into `0000` (and negative years into malformed text). Reject years outside the four-digit `format: date` range before truncating instead of emitting a different calendar date.</comment>
<file context>
@@ -371,9 +371,13 @@ export function serializeDateTime(value: Date): string {
+ throw new RangeError('Invalid time value');
+ }
+ // Not padStart: the generated client may target ES6, where it does not exist.
+ const year = ('000' + value.getFullYear()).slice(-4);
+ const month = ('0' + (value.getMonth() + 1)).slice(-2);
+ const day = ('0' + value.getDate()).slice(-2);
</file context>
| const year = ('000' + value.getFullYear()).slice(-4); | |
| const fullYear = value.getFullYear(); | |
| if (fullYear < 0 || fullYear > 9999) { | |
| throw new RangeError('Date year must be between 0000 and 9999'); | |
| } | |
| const year = ('000' + fullYear).slice(-4); |
| date.setHours(0, 0, 0, 0); | ||
| // Out-of-range components (or a day the local zone skipped) silently roll over, | ||
| // which would hand back a date the server never sent. | ||
| if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { |
There was a problem hiding this comment.
P2: On time zones that skip only local midnight, parseDate returns 01:00 instead of local midnight because the validation ignores the hour. The validation could also require date.getHours() === 0 and return Invalid Date when the requested midnight is not representable.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/others/typescript-fetch/self-import-issue/runtime.ts, line 402:
<comment>On time zones that skip only local midnight, `parseDate` returns 01:00 instead of local midnight because the validation ignores the hour. The validation could also require `date.getHours() === 0` and return `Invalid Date` when the requested midnight is not representable.</comment>
<file context>
@@ -383,10 +387,22 @@ export function parseDate(value: any): Date {
+ date.setHours(0, 0, 0, 0);
+ // Out-of-range components (or a day the local zone skipped) silently roll over,
+ // which would hand back a date the server never sent.
+ if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) {
+ return new Date(NaN);
+ }
</file context>
| if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { | |
| if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day || date.getHours() !== 0) { |
| // Out-of-range components (or a day the local zone skipped) silently roll over, | ||
| // which would hand back a date the server never sent. | ||
| if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { | ||
| return new Date(NaN); |
There was a problem hiding this comment.
P2: Malformed calendar dates still pass the generated oneOf date guard and become Invalid Date; aligning the oneOf guard with parseDate would prevent invalid date values from being selected as a valid branch.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache, line 394:
<comment>Malformed calendar dates still pass the generated oneOf date guard and become `Invalid Date`; aligning the oneOf guard with `parseDate` would prevent invalid date values from being selected as a valid branch.</comment>
<file context>
@@ -374,10 +378,22 @@ export function parseDate(value: any): Date {
+ // Out-of-range components (or a day the local zone skipped) silently roll over,
+ // which would hand back a date the server never sent.
+ if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) {
+ return new Date(NaN);
+ }
+ return date;
</file context>
| throw new RangeError('Invalid time value'); | ||
| } | ||
| // Not padStart: the generated client may target ES6, where it does not exist. | ||
| const year = ('000' + value.getFullYear()).slice(-4); |
There was a problem hiding this comment.
P2: A Date outside the four-digit year range is sent with a different year because slice(-4) truncates extended or negative years; rejecting years outside 0000–9999 before padding would avoid corrupting the request.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache, line 369:
<comment>A `Date` outside the four-digit year range is sent with a different year because `slice(-4)` truncates extended or negative years; rejecting years outside `0000–9999` before padding would avoid corrupting the request.</comment>
<file context>
@@ -362,9 +362,13 @@ export function serializeDateTime(value: Date): string {
+ throw new RangeError('Invalid time value');
+ }
+ // Not padStart: the generated client may target ES6, where it does not exist.
+ const year = ('000' + value.getFullYear()).slice(-4);
+ const month = ('0' + (value.getMonth() + 1)).slice(-2);
+ const day = ('0' + value.getDate()).slice(-2);
</file context>
| const year = ('000' + value.getFullYear()).slice(-4); | |
| const fullYear = value.getFullYear(); | |
| if (fullYear < 0 || fullYear > 9999) { | |
| throw new RangeError('Date year must be between 0000 and 9999'); | |
| } | |
| const year = ('000' + fullYear).slice(-4); |
There was a problem hiding this comment.
Negative years aren't representable in RFC 3339 (date-fullyear is 4DIGIT), so serializeDate doesn't try to handle them.
…mat: date
Date handling was spread across four templates, each inlining its own
expression. That had three consequences:
1. `format: date` was not handled at all for form parameters, so a Date
was appended raw and stringified by the browser
("Wed Aug 05 2026 00:00:00 GMT+0200 (…)") instead of "2026-08-05".
2. `format: date` shifted by a day everywhere except UTC, in one direction
or the other. Parsing (`new Date('2026-08-05')`) and serialising
(`.toISOString().substring(0, 10)`) both work in UTC, but consumers do
not: a date picker builds local midnight and display reads local
getters. West of UTC a date from the API displays as the previous day;
east of UTC a locally built date is sent as the previous day. An RFC
3339 full-date has no offset, so both ends have to use the same wall
clock — they now both use the local calendar. `format: date-time` is a
genuine instant and stays UTC.
3. Whether dates were represented as Date or string was decided by
`withoutRuntimeChecks`, an unrelated flag about payload validation.
All call sites (models, oneOf models, path/query/form parameters and the
querystring helper) now route through serializeDate/serializeDateTime and
parseDate/parseDateTime in runtime.ts, so the representation is defined in
one place. The new `dateLibrary` option (`date`, the default and previous
behaviour, or `string`) makes the choice explicit; `withoutRuntimeChecks`
implies `string`, as before, since there is no model code left to convert
with.
Adds a spec fixture covering every location a date can appear in, two
sample builds (one per option value), and tests for the option, the
fallback and the serialisation semantics.
…es, unused imports
- serializeDate no longer uses padStart, which is ES2017: the es6-target
sample did not compile against its own tsconfig.
- parseDate builds the local date with setFullYear, so years 0000-0099 keep
their century instead of picking up the multi-argument Date constructor's
1900 offset ("0045-08-05" was parsed as 1945).
- parseDate rejects components that roll over, so an out-of-range date or a
day the local zone skipped returns Invalid Date rather than a plausible
wrong one. Previously "2026-13-45" became 2027-02-14.
- serializeDate throws RangeError on an invalid Date instead of emitting
"0NaN-NaN-NaN", matching serializeDateTime.
- Models without a date property no longer import the date helpers, via a
new x-hasDateVars extension mirroring the template's own branches. This
reverts most of the sample churn from the previous commit.
Drops testDateFormatUsesTheLocalCalendar: runtime.ts is identical for every
spec, so asserting its body only restated the template. The per-build
tsconfig typecheck covers the ES6 regression properly.
The oneOf branches tested a value with `new Date(json)` but converted it with parseDate, so the two could disagree: "2026-02-30" passes the lenient test (V8 rolls it to March 2) and then parseDate rejects it, leaving the branch selected and returning Invalid Date. Testing with the same helper that does the conversion lets the oneOf fall through to another branch instead. Adds a oneOf date member to the date-handling fixture. No sample in the repo exercised these branches, so the generated form of the guard was invisible in the samples; only the scalar variant is left out, because a scalar oneOf primitive already fails `tsc --strict` on master (it can return undefined, which is not in the union). Drops the comment justifying the ES6-safe padding: the per-build tsconfig typecheck already fails if someone reaches for padStart again.
… guards into the date helpers Upstream OpenAPITools#24509 null-guarded required date properties; keep that guard but route the conversion through parseDate/serializeDate. Port the form param date handling into the apisFormParams partial extracted by OpenAPITools#23935, and regenerate the affected samples. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
f83ab68 to
61a343b
Compare
macjohnny
left a comment
There was a problem hiding this comment.
thanks for consolidating this date handling!
| if (isNaN(value.getTime())) { | ||
| throw new RangeError('Invalid time value'); | ||
| } | ||
| const year = ('000' + value.getFullYear()).slice(-4); |
There was a problem hiding this comment.
nit: how about using https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart?
can be done in a follow-up
There was a problem hiding this comment.
I tried at first. But the compiler complained as String.prototype.padStart is ES2017, and the generated client should target ES6/ES2015.
So I reverted it and use this technique instead :)
Happy to change if you have another path to suggest though
…runtime.mustache Co-authored-by: Esteban Gehring <esteban.gehring@gmail.com>
|
please update the samples |
Fixes #24636
Short version:
typescript-fetchwrites its date conversion out by hand in four templates plusquerystring, and the copies have drifted.format: dateends up unconverted for form parameters, and everywhere else it goes through UTC, which moves the day for any code using the local half of theDateAPI.The issue has the measurements.
Design:
dateas a local date, keep UTC fordate-time.every timezone except UTC, in one direction or the other.
one the surrounding code already uses:
new Date(Y, M, D),getDate()andtoLocaleDateString()are all local. It also lines up with the other generators,where
format: datemaps toLocalDate,NaiveDateordatetime.dateand the dayyou read back is the day the server sent.
runtime.tsso that every path uses the sameserialization/deserialization
isDateTypebranch for form parameters, the only location that wasconverting nothing at all.
dateLibraryto choose howdateanddate-timearerepresented.
datefor a nativeDateconverted by the runtime, andstringas anexplicit escape hatch, handing back exactly what the server sent for consumers who
want to own date handling (SSR/RSC, or an app that already has a date library)
withoutRuntimeChecksstill impliesstring. Maybe you'd like it to be explicit?datehelpers if we usedateLibrary: date.serializeDateTimealways ships, since
querystringcan be handed aDateeither wayTemporal.PlainDate/Temporal.Instantwould remove the wall-clock choice entirely andis the proper fix here, but it can't be the default while Safari hasn't shipped it. I have
a follow-up drafted to add it as a third
dateLibraryvalue. Would you rather have it inthis PR?
Behaviour change
Today a
format: dateis parsed to the instant of UTC midnight. This PR parses it tolocal midnight instead, which is the position @macjohnny took in #17284 ("someone uses a
local date and expects the local representation to be transferred over the wire"). That
issue and #17272 ask the same question for the
typescriptgenerator and have sat sinceDecember 2023. This PR only touches
typescript-fetch.Changing the anchor is not free. Reading the value as a plain date keeps working, or
starts working. Anything that relied on the instant being UTC midnight changes:
So it breaks consumers who read dates with
getUTC*, compare them againstDate.UTC(...), or persist and log the raw instant. Server-side rendering is affected too,since the instant now depends on the process time zone.
dateLibrary: stringis the way out, and it reproduces the current behaviour exactly:new Date(value)andvalue.toISOString().substring(0, 10)are what the generator doestoday.
Testing
TypeScriptFetchClientCodegenTest, covering the default,dateLibrary: string, thewithoutRuntimeChecksfallback, serialization per parameterlocation, and the local-calendar semantics.
TypeScriptFetchClientOptionsTestand itsprovider pick up the new option. The whole typescript group passes: 192 tests.
3_0/typescript-fetch/date-handling.yamlwith aformat: datein a modelproperty and in a path, query and form parameter, plus one sample build per option value
so the difference between them is a readable diff.
tsc --strictover all 19typescript-fetchsample builds: 15 clean, 4 with the sameerror counts as master (a
123operationId, and missingimmutable/redux-ts-simple).UTC, Europe/Paris, Asia/Tokyo and Pacific/Auckland. Round-trip, date-picker input and
display are correct in every one. Before, display was wrong west of UTC and the
date-picker path was wrong east of it.
PR checklist
./mvnw clean package,./bin/generate-samples.sh ./bin/configs/typescript-fetch*.yamland./bin/utils/export_generator.sh typescript-fetch, and committed all changed files.Summary by cubic
Centralized date handling in
typescript-fetch, added adateLibraryoption, and fixedformat: dateserialization across path/query/form params and querystrings.datenow uses the local calendar;date-timestays UTC, with ES6-safe helpers that validate edge cases.New Features
dateLibraryoption:date(default, nativeDate) orstring(pass-through).withoutRuntimeChecksimpliesstring.parseDate,parseDateTime,serializeDate,serializeDateTimeused in models, oneOf guards, and all parameter locations.padStart), preserves years 0000–0099, rejects invalid/DST-skipped dates, throws on invalidDate. Models without date fields no longer import helpers.Migration
format: dateis now parsed/serialized as a local date, not UTC midnight.dateLibrary: stringor update code that usesgetUTC*/toISOString()for plain dates.Written for commit dddd45d. Summary will update on new commits.