[BUG][typescript-fetch] format: date handling is inconsistent and shifts by a day west of UTC
Bug Report Checklist
Description
typescript-fetch inlines its date (de)serialization separately in four templates
(modelGeneric.mustache, modelOneOf.mustache, apis.mustache,
apisAssignQueryParam.mustache) and in runtime.mustache's querystring. Because
each site spells the conversion out by hand, they have drifted apart, which produces
three distinct problems.
1. format: date is not handled at all for form parameters
apis.mustache has an isDateTimeType branch for form params but no isDateType
branch, so a format: date parameter falls through to the primitive branch and the
Date is appended to FormData unconverted:
formParams.append('startsOn', requestParameters['startsOn'] as any);
The browser stringifies it with Date.prototype.toString(), so the request body
carries startsOn=Wed+Aug+05+2026+00%3A00%3A00+GMT%2B0200+(Central+European+Summer+Time)
instead of startsOn=2026-08-05. Every other location (path, query, model) does
convert, so this is inconsistent within the same generator.
2. A format: date round-trip loses a day west of UTC
An RFC 3339 full-date is a calendar date: no time, no offset. A JS Date is an
instant, so representing one means picking a wall clock — and parsing and serializing
must pick the same one. Currently they don't:
- parse:
new Date('2026-08-05') — a date-only string is specified to parse as UTC
- serialize:
value.toISOString().substring(0, 10) — the UTC calendar day
Those two agree with each other, but not with any locally-built Date, which is what
a date picker or new Date(2026, 7, 5) produces. And getDate()-style display of a
parsed value is wrong west of UTC:
// TZ=America/New_York
const fromApi = new Date('2026-08-05'); // 2026-08-04T20:00:00-04:00
fromApi.getDate(); // 4 ← displayed as the 4th
fromApi.toISOString().substring(0, 10); // '2026-08-05' (round-trips, but…)
const fromPicker = new Date(2026, 7, 5); // 2026-08-05T00:00:00-04:00
fromPicker.toISOString().substring(0, 10);// '2026-08-04' ← sends the wrong day
So today a user either displays the wrong day or sends the wrong day, depending on
where the Date came from. If the API says the 5th of August, the client should show
and send the 5th of August in every time zone.
3. Date-vs-string representation is decided by an unrelated flag
processOpts maps date/DateTime to Date only inside if (!withoutRuntimeChecks).
withoutRuntimeChecks is documented as being about runtime validation of payloads, yet
it silently also switches the type of every date field to string. There is no way to
ask for string dates while keeping runtime checks — which is what you want for SSR/RSC
(a Date is not serializable across the server/client boundary), or when the consumer
owns date parsing (Luxon, day.js, Temporal).
openapi-generator version
master (7.25.0-SNAPSHOT). Present in every 7.x release; the form-parameter gap and the
UTC/local asymmetry are long-standing.
OpenAPI declaration file
openapi: 3.0.3
info: { title: Date handling, version: 1.0.0 }
paths:
/events/{onDate}:
get:
operationId: listEvents
parameters:
- { name: onDate, in: path, required: true, schema: { type: string, format: date } }
- { name: from, in: query, schema: { type: string, format: date } }
responses: { '200': { description: ok } }
/events:
post:
operationId: createEvent
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
required: [startsOn]
properties:
startsOn: { type: string, format: date }
createdAt: { type: string, format: date-time }
responses: { '200': { description: ok } }
Generation details
openapi-generator-cli generate -g typescript-fetch -i date-handling.yaml -o out
Steps to reproduce
- Generate with the spec above.
grep -n "formParams.append('startsOn'" out/apis/DefaultApi.ts — no conversion (problem 1).
- In
TZ=America/New_York, run EventToJSON({ startsOn: new Date(2026, 7, 5) }) — get
2026-08-04 (problem 2).
Related issues/PRs
Same root cause, previously reported for individual call sites:
Suggest a fix
Two parts, in one PR: #24637
Centralise. Emit one helper set in runtime.ts — serializeDate,
serializeDateTime, parseDate, parseDateTime — and route every call site through
it, so the representation is defined once and cannot drift again. That also closes the
missing form-parameter branch.
Make the semantics symmetric. format: date uses the local calendar on both ends
(getFullYear/getMonth/getDate out, new Date(y, m - 1, d) in), so a stated day
round-trips and displays as that day in every time zone. format: date-time is a
genuine instant and stays toISOString().
Make the representation an explicit option. A new dateLibrary flag, matching the
naming already used by the java/kotlin/dart generators:
| value |
behaviour |
date (default) |
native Date, converted by the runtime — today's behaviour, minus the bugs |
string |
pass values through untouched, consumer owns date handling |
withoutRuntimeChecks: true keeps implying string, since with no model code there is
nothing to convert with; passing dateLibrary: date alongside it warns and falls back.
The default is unchanged, so this is not a breaking change for anyone whose dates were
already correct.
[BUG][typescript-fetch]
format: datehandling is inconsistent and shifts by a day west of UTCBug Report Checklist
Description
typescript-fetchinlines its date (de)serialization separately in four templates(
modelGeneric.mustache,modelOneOf.mustache,apis.mustache,apisAssignQueryParam.mustache) and inruntime.mustache'squerystring. Becauseeach site spells the conversion out by hand, they have drifted apart, which produces
three distinct problems.
1.
format: dateis not handled at all for form parametersapis.mustachehas anisDateTimeTypebranch for form params but noisDateTypebranch, so a
format: dateparameter falls through to the primitive branch and theDateis appended toFormDataunconverted:The browser stringifies it with
Date.prototype.toString(), so the request bodycarries
startsOn=Wed+Aug+05+2026+00%3A00%3A00+GMT%2B0200+(Central+European+Summer+Time)instead of
startsOn=2026-08-05. Every other location (path, query, model) doesconvert, so this is inconsistent within the same generator.
2. A
format: dateround-trip loses a day west of UTCAn RFC 3339
full-dateis a calendar date: no time, no offset. A JSDateis aninstant, so representing one means picking a wall clock — and parsing and serializing
must pick the same one. Currently they don't:
new Date('2026-08-05')— a date-only string is specified to parse as UTCvalue.toISOString().substring(0, 10)— the UTC calendar dayThose two agree with each other, but not with any locally-built
Date, which is whata date picker or
new Date(2026, 7, 5)produces. AndgetDate()-style display of aparsed value is wrong west of UTC:
So today a user either displays the wrong day or sends the wrong day, depending on
where the
Datecame from. If the API says the 5th of August, the client should showand send the 5th of August in every time zone.
3. Date-vs-string representation is decided by an unrelated flag
processOptsmapsdate/DateTimetoDateonly insideif (!withoutRuntimeChecks).withoutRuntimeChecksis documented as being about runtime validation of payloads, yetit silently also switches the type of every date field to
string. There is no way toask for
stringdates while keeping runtime checks — which is what you want for SSR/RSC(a
Dateis not serializable across the server/client boundary), or when the consumerowns date parsing (Luxon, day.js, Temporal).
openapi-generator version
master (7.25.0-SNAPSHOT). Present in every 7.x release; the form-parameter gap and the
UTC/local asymmetry are long-standing.
OpenAPI declaration file
Generation details
Steps to reproduce
grep -n "formParams.append('startsOn'" out/apis/DefaultApi.ts— no conversion (problem 1).TZ=America/New_York, runEventToJSON({ startsOn: new Date(2026, 7, 5) })— get2026-08-04(problem 2).Related issues/PRs
Same root cause, previously reported for individual call sites:
Suggest a fix
Two parts, in one PR: #24637
Centralise. Emit one helper set in
runtime.ts—serializeDate,serializeDateTime,parseDate,parseDateTime— and route every call site throughit, so the representation is defined once and cannot drift again. That also closes the
missing form-parameter branch.
Make the semantics symmetric.
format: dateuses the local calendar on both ends(
getFullYear/getMonth/getDateout,new Date(y, m - 1, d)in), so a stated dayround-trips and displays as that day in every time zone.
format: date-timeis agenuine instant and stays
toISOString().Make the representation an explicit option. A new
dateLibraryflag, matching thenaming already used by the java/kotlin/dart generators:
date(default)Date, converted by the runtime — today's behaviour, minus the bugsstringwithoutRuntimeChecks: truekeeps implyingstring, since with no model code there isnothing to convert with; passing
dateLibrary: datealongside it warns and falls back.The default is unchanged, so this is not a breaking change for anyone whose dates were
already correct.