Skip to content

[BUG][typescript-fetch] format: date handling is inconsistent and shifts by a day west of UTC #24636

Description

@b2l

[BUG][typescript-fetch] format: date handling is inconsistent and shifts by a day west of UTC

Bug Report Checklist

  • Have you provided a full/minimal spec to reproduce the issue?
  • Have you validated the input using an OpenAPI validator (example)?
  • Have you tested with the latest master to confirm the issue still exists?
  • Have you searched for related issues/PRs?
  • What's the actual output vs expected output?
  • [Optional] Sponsorship to speed up the bug fix or feature request (example)

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

  1. Generate with the spec above.
  2. grep -n "formParams.append('startsOn'" out/apis/DefaultApi.ts — no conversion (problem 1).
  3. 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.tsserializeDate,
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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions