Quick answer: Most OpenAPI generators map format: date-time to a plain string rather than a real date object, which is convenient for the type system and dangerous for the application: every consumer assumes someone else parsed the timestamp, and raw strings get compared, sorted, and sent back to the server without ever crossing a date library. Timezone bugs, failed ordering, and a silent display of UTC with a local label follow. The fix is to make the boundary explicit: configure the generator to produce a real temporal type where supported, or centralize parsing in one validated layer so the rest of the code never handles a raw timestamp. A date-time field is a value with rules, not free text, and treating it as a string pushes those rules onto every caller.

The generator's choice is understandable. JSON has no date literal, so a timestamp arrives over the wire as a string regardless of the language, and emitting a string avoids imposing a parsing library or guessing the desired date type. The problem is that the generated type then declares the field as string, which is technically accurate at the transport layer but says nothing about the constraint. Downstream code reads event.start_time, sees a string, and happily treats it as one, while the format contract is never enforced in the type.

How raw date-time strings break behavior

The failures are quiet because none of them throw at the boundary; they produce wrong results later.

OperationWith a raw stringWith a parsed temporal type
ComparisonLexicographic, format-dependentChronological
SortingBreaks across offsetsCorrect by instant
DisplayUTC shown as if localConverted with zone
Round-tripReformatted inconsistentlySerialized to spec
ValidationInvalid text acceptedParse fails loudly

The comparison trap is the most underestimated. Two ISO strings that share the same format and offset compare correctly lexicographically, which makes string sorting look safe in a narrow test. The instant one value carries a different offset or a different precision, the string ordering diverges from chronological order, and the bug only appears against mixed real data.

Generate real temporal types where possible

Several generators support mapping date-time to a native type, either through a built-in option or a type mapping. Prefer that so the field is parsed into a value that cannot be compared lexicographically by accident.

# openapi-generator: emit native temporal types instead of strings
java -jar openapi-generator-cli.jar generate \
  -i openapi.yaml \
  -g typescript-fetch \
  --type-mappings=DateTime=Date \
  --additional-properties=useDateType=true

# the generated field now reflects a real boundary
export interface Event {
  start_time: Date;     // parsed, not a raw string
}

When the generator supports a modern, timezone-aware library, prefer it over the legacy mutable date type, because the legacy type lacks explicit zone handling and reintroduces the display ambiguity. The goal is a type that models an instant and forces conversion at the edges.

Centralize parsing when the generator cannot

If the available generator only emits strings, do not scatter parsing through the codebase. Add one decoding layer that validates and converts every timestamp as the response enters, and a matching encoder for outbound requests. The rest of the application then sees only parsed values.

import { z } from 'zod';

function isIso(value: string): boolean {
  const parsed = new Date(value);
  return !Number.isNaN(parsed.getTime()) && value.includes('T');
}

// validated at the boundary; an invalid value throws here, nowhere else
const eventSchema = z.object({
  id: z.string(),
  start_time: z.string().refine(isIso, 'invalid date-time').transform((s) => new Date(s)),
  end_time: z.string().refine(isIso, 'invalid date-time').transform((s) => new Date(s)),
});

type Event = z.infer<typeof eventSchema>;   // start_time: Date

export async function getEvent(id: string): Promise<Event> {
  const raw = await fetch(`/v1/events/${id}`).then((r) => r.json());
  return eventSchema.parse(raw);
}

Centralizing the parse means there is exactly one place that knows the wire format and exactly one place an invalid timestamp can be rejected. Outbound serialization should live in the same module so the round-trip stays consistent, instead of each call site reformatting a string by hand.

Keep display and storage concerns separate

A parsed instant is still not a display value. Formatting for a user requires a chosen timezone and locale, and that conversion belongs in the presentation layer, close to the component, not in the data model. Keeping the internal representation as an unambiguous instant and formatting only at render prevents the classic bug where a UTC value is labeled with the local time without being converted.

// render-time conversion only; the stored value stays an instant
function formatStart(event: Event, timeZone: string): string {
  return new Intl.DateTimeFormat('en-US', {
    dateStyle: 'medium',
    timeStyle: 'short',
    timeZone,
  }).format(event.start_time);
}

A local-first code generation workflow ties this together: the OpenAPI spec declares the date-time format, generated clients respect the chosen type mapping, and the boundary schema enforces what the type alone cannot. The principle is that constraints should be enforced once, at the contract edge, rather than re-created as assumptions in every consumer.

Frequently asked questions

Is a string type really wrong if JSON sends strings?

The wire format is a string, but the field carries a format constraint. A generated string type loses that constraint, so parsing and validation need to happen somewhere. The question is whether that happens once at the boundary or by accident in every caller.

Why does string sorting ever work?

ISO strings with identical precision and offset sort lexicographically in chronological order, which masks the problem in simple tests. Mixed offsets or different precision break the equivalence, so the safe practice is to sort parsed instants.

What if my generator cannot emit date types?

Add a single validation and decoding layer using a schema library, and expose only parsed types to the rest of the code. Avoid spreading parsing logic across components, which recreates the inconsistency you are trying to remove.

Should I store the timestamp as a string internally?

Keep an unambiguous instant internally, whether that is a date object or a library type. Convert to a display string only at presentation and to a wire string only at the boundary.

How does the spec prevent these bugs?

It records the date-time format as the source of truth, allowing generators and validators to apply a consistent type mapping and reject malformed values. The spec is what makes the constraint enforceable rather than a convention people hope to follow.

This is part of the Code Generation Guide — start there for the full picture of keeping generated clients honest about their data.