Skip to content

[dart] Do not convert format: date to UTC before formatting - #24706

Open
eirikb wants to merge 2 commits into
OpenAPITools:masterfrom
eirikb:feature/eirikb/dart-date-no-utc-shift
Open

[dart] Do not convert format: date to UTC before formatting#24706
eirikb wants to merge 2 commits into
OpenAPITools:masterfrom
eirikb:feature/eirikb/dart-date-no-utc-shift

Conversation

@eirikb

@eirikb eirikb commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

fix #24703

The dart generator emits this for a format: date property:

json[r'dueDate'] = _dateFormatter.format(this.dueDate!.toUtc());

_dateFormatter is DateFormat('yyyy-MM-dd'), which formats the y/m/d the DateTime already carries. It does no timezone conversion, so that .toUtc() does nothing except move the value across a day boundary before the date is read.

Coming the other way, mapDateTime parses the bare wire value "2026-09-12" with DateTime.tryParse, which gives you local midnight. Round trip in Oslo:

"2026-09-12" -> DateTime 2026-09-12 00:00 local -> toUtc() 2026-09-11 22:00Z -> "2026-09-11"

It is stable in UTC, which I bet is why this has been around so long. We only noticed because a payment deadline got printed one day early on a physical parking fine.

Removing .toUtc() should be safe in both directions. toUtc() returns this when the DateTime is already UTC, so UTC callers are unaffected, and local callers stop being shifted. The isDateTime branch right next to it keeps its .toUtc(), an instant needs a zone, a calendar date does not.

FWIW dart-dio already handles this correctly with its own Date class (plain year/month/day ints), so the two dart generators disagree about this today.

Tests

Added a DateOnlyModel fixture and two tests that between them pin all four changed template lines:

branch covered by
plain, no pattern requiredDate, optionalDate
plain, with pattern patternedDate
Optional<T>, no pattern optionalDate with useOptional=true
Optional<T>, with pattern patternedDate with useOptional=true

Both tests fail against the old template and pass with the fix. Samples regenerated, it comes out as two lines.

Not fixed here

While I was in there I noticed format: date is also wrong in a few other places: query, header and form parameters go through parameterToString, which is type-erased and cannot tell date from date-time, path parameters use plain .toString(), and arrays of dates do not compile at all (DateTime.listFromJson does not exist). There is also an epoch-marker branch that still normalizes to UTC, and _dateFormatter has no explicit locale so it can emit non-ASCII digits if Intl.defaultLocale is set.

Those are separate defects with separate fixes and much bigger diffs, so I left them alone to keep this one small. Happy to look at them after if you want.

PR checklist

  • Read the contribution guidelines.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (I ran the two dart configs, dart-petstore-client-lib.yaml and dart-petstore-client-lib-fake.yaml, since the template only affects dart native.)
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

@jaumard @amondnet @sbu-WBT @kuhnroyal @agilob @ahmednfwela


Summary by cubic

Stop converting Dart format: date fields to UTC before formatting. Previously we emitted _dateFormatter.format(value.toUtc()), which shifted local-midnight dates across day boundaries and serialized the wrong day; now we call _dateFormatter.format(value). The format: date-time path is unchanged and still uses .toUtc().

  • Review notes

    • Update modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache to remove .toUtc() in all isDate branches (plain and pattern).
    • Add tests in DartClientCodegenTest plus a DateOnlyModel fixture to cover plain/pattern and useOptional=true paths; assertions check the exact emitted lines.
    • Regenerate samples: one-line changes in format_test.dart and nullable_class.dart.
  • Impact

    • No migration required for correct clients; serialized dates now match the calendar date carried by the DateTime.
    • If any code compensated for the previous off-by-one shift, remove those workarounds.

Written for commit e27bf96. Summary will update on new commits.

Review in cubic

eirikb added 2 commits August 14, 2026 08:46
_dateFormatter is DateFormat('yyyy-MM-dd'), which formats the y/m/d the
DateTime already carries and performs no timezone conversion. Calling
toUtc() first therefore does nothing except roll the clock back past
midnight in UTC+X zones, so the formatter prints the previous day.

Combined with mapDateTime parsing the bare wire value "2026-09-12" as
local midnight, the round trip in Europe/Oslo is "2026-09-12" ->
"2026-09-11". It is stable in UTC, which is why this went unnoticed.

Removing toUtc() is safe in both directions: toUtc() returns this when
the DateTime is already UTC, so UTC callers are unaffected, and local
callers stop being shifted. The neighbouring isDateTime branch keeps
toUtc() - an instant needs a zone, a calendar date does not. dart-dio
already models this correctly with its own Date class.

fix OpenAPITools#24703
The first test only exercised the plain, no-pattern branch, so three of the
four changed template lines were unguarded. Adding a patterned date to the
fixture plus a second test that generates with useOptional=true pins all
four:

  plain / no pattern      requiredDate, optionalDate
  plain / pattern         patternedDate
  Optional / no pattern   optionalDate   (useOptional=true)
  Optional / pattern      patternedDate  (useOptional=true)

Replace the blanket assertFileNotContains(".toUtc()") with assertions on the
specific emitted lines. The blanket one would break if an unrelated
date-time property were ever added to the shared fixture.

Also correct the comment: toUtc() shifts the date in both directions, back a
day east of UTC and forward a day west of it, not only backwards.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 5 files

Re-trigger cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG][DART] format: date fields shift one day back in timezones east of UTC

2 participants