OpenAPITools / OpenAPITools/openapi-generator

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

Open
#24,635 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Issue: Bug
Dominant language
Java
Stars
26.8k
Forks
7.7k
PR merge metrics
PR metrics pending

Description

[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:

  • #9805, #12134 (date shifted by one day)
  • #7651 (date/date-time representation)

Suggest a fix

Two parts, in one PR:

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by generating the supplied date-handling.yaml with the typescript-fetch generator, then inspect modelGeneric.mustache, modelOneOf.mustache, apis.mustache, apisAssignQueryParam.mustache, and runtime.mustache. Reproduce the form-parameter and America/New_York cases from the issue; done means date handling is consistent across the named call sites and the explicit dateLibrary behavior is covered without changing the documented default.

Written by the indexing model from the issue text.

Assessment

Tech stack
openapi, typescript
Domain
api, tooling
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.