Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/generators/typescript-fetch.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
| Option | Description | Values | Default |
| ------ | ----------- | ------ | ------- |
|allowUnicodeIdentifiers|boolean, toggles whether unicode identifiers are allowed in names or not, default is false| |false|
|dateLibrary|Option. Date library to use.|<dl><dt>**date**</dt><dd>Native Date. `format: date` and `format: date-time` are both mapped to Date and (de)serialized by the runtime.</dd><dt>**string**</dt><dd>Plain string. Values are passed through untouched, leaving date handling to the consumer.</dd></dl>|date|
|dateLibrary|Option. Date library to use.|<dl><dt>**date**</dt><dd>Native Date. `format: date` and `format: date-time` are both mapped to Date and (de)serialized by the runtime.</dd><dt>**string**</dt><dd>Plain string. Values are passed through untouched, leaving date handling to the consumer.</dd><dt>**temporal**</dt><dd>Native Temporal. `format: date` is mapped to Temporal.PlainDate and `format: date-time` is mapped to Temporal.Instant and (de)serialized by the runtime.</dd></dl>|date|
|disallowAdditionalPropertiesIfNotPresent|If false, the 'additionalProperties' implementation (set to true by default) is compliant with the OAS and JSON schema specifications. If true (default), keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.|<dl><dt>**false**</dt><dd>The 'additionalProperties' implementation is compliant with the OAS and JSON schema specifications.</dd><dt>**true**</dt><dd>Keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.</dd></dl>|true|
|ensureUniqueParams|Whether to ensure parameter names are unique in an operation (rename parameters that are not).| |true|
|enumNameSuffix|Suffix that will be appended to all enum names.| |Enum|
Expand Down Expand Up @@ -99,6 +99,8 @@ These options may be applied as additional-properties (cli) or configOptions (pl
<li>ReturnType</li>
<li>Set</li>
<li>String</li>
<li>Temporal.Instant</li>
<li>Temporal.PlainDate</li>
<li>ThisParameterType</li>
<li>ThisType</li>
<li>Uncapitalize</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public class TypeScriptFetchClientCodegen extends AbstractTypeScriptClientCodege
public static final String DATE_LIBRARY_DESC = "Option. Date library to use.";
public static final String DATE_LIBRARY_DATE = "date";
public static final String DATE_LIBRARY_STRING = "string";
public static final String DATE_LIBRARY_TEMPORAL = "temporal";
public static final String STRING_ENUMS = "stringEnums";
public static final String STRING_ENUMS_DESC = "Generate string enums instead of objects for enum values.";
public static final String IMPORT_FILE_EXTENSION_SWITCH = "importFileExtension";
Expand Down Expand Up @@ -112,6 +113,8 @@ public class TypeScriptFetchClientCodegen extends AbstractTypeScriptClientCodege
private static final String DATE_TYPE = "date";
private static final String DATE_TIME_TYPE = "DateTime";
private static final String TS_DATE_TYPE = "Date";
private static final String TS_TEMPORAL_INSTANT_TYPE = "Temporal.Instant";
private static final String TS_TEMPORAL_PLAIN_DATE_TYPE = "Temporal.PlainDate";

protected boolean sagasAndRecords = false;
@Getter @Setter
Expand Down Expand Up @@ -143,6 +146,11 @@ public TypeScriptFetchClientCodegen() {

this.addExtraReservedWords();

languageSpecificPrimitives.addAll(Arrays.asList(
TS_TEMPORAL_INSTANT_TYPE,
TS_TEMPORAL_PLAIN_DATE_TYPE
));

supportModelPropertyNaming(CodegenConstants.MODEL_PROPERTY_NAMING_TYPE.camelCase);
this.cliOptions.add(new CliOption(NPM_REPOSITORY, "Use this property to set an url your private npmRepo in the package.json"));
this.cliOptions.add(new CliOption(WITH_INTERFACES, "Setting this property to true will generate interfaces next to the default class implementations.", SchemaTypeUtil.BOOLEAN_TYPE).defaultValue(Boolean.FALSE.toString()));
Expand All @@ -154,6 +162,7 @@ public TypeScriptFetchClientCodegen() {
Map<String, String> dateOptions = new HashMap<>();
dateOptions.put(DATE_LIBRARY_DATE, "Native Date. `format: date` and `format: date-time` are both mapped to Date and (de)serialized by the runtime.");
dateOptions.put(DATE_LIBRARY_STRING, "Plain string. Values are passed through untouched, leaving date handling to the consumer.");
dateOptions.put(DATE_LIBRARY_TEMPORAL, "Native Temporal. `format: date` is mapped to Temporal.PlainDate and `format: date-time` is mapped to Temporal.Instant and (de)serialized by the runtime.");
dateLibraryOption.setEnum(dateOptions);
this.cliOptions.add(dateLibraryOption);
this.cliOptions.add(new CliOption(SAGAS_AND_RECORDS, "Setting this property to true will generate additional files for use with redux-saga and immutablejs.", SchemaTypeUtil.BOOLEAN_TYPE).defaultValue(Boolean.FALSE.toString()));
Expand Down Expand Up @@ -349,24 +358,29 @@ public void processOpts() {

// `date` needs the model (de)serialization to convert with, which
// withoutRuntimeChecks removes: the raw string would just be cast to Date.
if (withoutRuntimeChecks && DATE_LIBRARY_DATE.equals(this.dateLibrary)) {
if (withoutRuntimeChecks && (DATE_LIBRARY_DATE.equals(this.dateLibrary) || DATE_LIBRARY_TEMPORAL.equals(this.dateLibrary))) {
if (additionalProperties.containsKey(DATE_LIBRARY)) {
LOGGER.warn("{}={} is not compatible with {}=true; falling back to {}={}.",
DATE_LIBRARY, DATE_LIBRARY_DATE, WITHOUT_RUNTIME_CHECKS, DATE_LIBRARY, DATE_LIBRARY_STRING);
DATE_LIBRARY, this.dateLibrary, WITHOUT_RUNTIME_CHECKS, DATE_LIBRARY, DATE_LIBRARY_STRING);
}
this.dateLibrary = DATE_LIBRARY_STRING;
}

if (DATE_LIBRARY_DATE.equals(this.dateLibrary)) {
typeMapping.put(DATE_TYPE, TS_DATE_TYPE);
typeMapping.put(DATE_TIME_TYPE, TS_DATE_TYPE);
} else if (DATE_LIBRARY_TEMPORAL.equals(this.dateLibrary)) {
typeMapping.put(DATE_TYPE, TS_TEMPORAL_PLAIN_DATE_TYPE);
typeMapping.put(DATE_TIME_TYPE, TS_TEMPORAL_INSTANT_TYPE);
} else {
typeMapping.put(DATE_TYPE, "string");
typeMapping.put(DATE_TIME_TYPE, "string");
}
additionalProperties.put(DATE_LIBRARY, this.dateLibrary);
// Mustache cannot compare strings, so expose the selected library as a flag.
additionalProperties.put("isDateLibraryDate", DATE_LIBRARY_DATE.equals(this.dateLibrary));
additionalProperties.put("isDateLibraryString", DATE_LIBRARY_STRING.equals(this.dateLibrary));
additionalProperties.put("isDateLibraryTemporal", DATE_LIBRARY_TEMPORAL.equals(this.dateLibrary));

if (additionalProperties.containsKey(SAGAS_AND_RECORDS)) {
this.setSagasAndRecords(convertPropertyToBoolean(SAGAS_AND_RECORDS));
Expand Down Expand Up @@ -2125,10 +2139,10 @@ protected String getLicenseNameDefaultValue() {
}

private static boolean isDateType(String dataType) {
return TS_DATE_TYPE.equals(dataType);
return TS_DATE_TYPE.equals(dataType) || TS_TEMPORAL_PLAIN_DATE_TYPE.equals(dataType);
}

private static boolean isDateTimeType(String dataType) {
return TS_DATE_TYPE.equals(dataType);
return TS_DATE_TYPE.equals(dataType) || TS_TEMPORAL_INSTANT_TYPE.equals(dataType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -307,16 +307,28 @@ export class {{classname}} extends runtime.BaseAPI {
let urlPath = `{{{path}}}`;
{{#pathParams}}
{{#isDateTimeType}}
{{#isDateLibraryTemporal}}
if (requestParameters['{{paramName}}'] instanceof Temporal.Instant) {

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.

P2: When dateLibrary=temporal is used for a path parameter, the generated API references Temporal without providing its TypeScript declaration. Add an explicit Temporal type dependency/reference, or otherwise generate the required ambient typing so the generated package builds without manual consumer setup.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache, line 311:

<comment>When `dateLibrary=temporal` is used for a path parameter, the generated API references `Temporal` without providing its TypeScript declaration. Add an explicit Temporal type dependency/reference, or otherwise generate the required ambient typing so the generated package builds without manual consumer setup.</comment>

<file context>
@@ -307,16 +307,28 @@ export class {{classname}} extends runtime.BaseAPI {
         {{#pathParams}}
         {{#isDateTimeType}}
+        {{#isDateLibraryTemporal}}
+        if (requestParameters['{{paramName}}'] instanceof Temporal.Instant) {
+            urlPath = urlPath.replace({{=<< >>=}}'{<<baseName>>}'<<={{ }}=>>, encodeURIComponent(runtime.serializeDateTime(requestParameters['{{paramName}}'])));
+        {{/isDateLibraryTemporal}}
</file context>

urlPath = urlPath.replace({{=<< >>=}}'{<<baseName>>}'<<={{ }}=>>, encodeURIComponent(runtime.serializeDateTime(requestParameters['{{paramName}}'])));
{{/isDateLibraryTemporal}}
{{^isDateLibraryTemporal}}
if (requestParameters['{{paramName}}'] instanceof Date) {
urlPath = urlPath.replace({{=<< >>=}}'{<<baseName>>}'<<={{ }}=>>, encodeURIComponent(runtime.serializeDateTime(requestParameters['{{paramName}}'])));
{{/isDateLibraryTemporal}}
} else {
urlPath = urlPath.replace({{=<< >>=}}'{<<baseName>>}'<<={{ }}=>>, encodeURIComponent(String(requestParameters['{{paramName}}'])));
}
{{/isDateTimeType}}
{{^isDateTimeType}}
{{#isDateType}}
{{#isDateLibraryTemporal}}
if (requestParameters['{{paramName}}'] instanceof Temporal.PlainDate) {
urlPath = urlPath.replace({{=<< >>=}}'{<<baseName>>}'<<={{ }}=>>, encodeURIComponent(runtime.serializeDate(requestParameters['{{paramName}}'])));
{{/isDateLibraryTemporal}}
{{^isDateLibraryTemporal}}
if (requestParameters['{{paramName}}'] instanceof Date) {
urlPath = urlPath.replace({{=<< >>=}}'{<<baseName>>}'<<={{ }}=>>, encodeURIComponent(runtime.serializeDate(requestParameters['{{paramName}}'])));
{{/isDateLibraryTemporal}}
} else {
urlPath = urlPath.replace({{=<< >>=}}'{<<baseName>>}'<<={{ }}=>>, encodeURIComponent(String(requestParameters['{{paramName}}'])));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mapValues{{#isDateLibraryDate}}{{#vendorExtensions.x-hasDateVars}}, parseDate, parseDateTime, serializeDate, serializeDateTime{{/vendorExtensions.x-hasDateVars}}{{/isDateLibraryDate}} } from '../runtime{{importFileExtension}}';
import { mapValues{{^isDateLibraryString}}{{#vendorExtensions.x-hasDateVars}}, parseDate, parseDateTime, serializeDate, serializeDateTime{{/vendorExtensions.x-hasDateVars}}{{/isDateLibraryString}} } from '../runtime{{importFileExtension}}';
{{#hasImports}}
{{#tsImports}}
import type { {{{classname}}} } from './{{filename}}{{importFileExtension}}';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{{#isDateLibraryDate}}
{{^isDateLibraryString}}
import { parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime{{importFileExtension}}';
{{/isDateLibraryDate}}
{{/isDateLibraryString}}
{{#hasImports}}
{{#oneOfArrays}}
import type { {{{.}}} } from './{{.}}{{importFileExtension}}';
Expand Down Expand Up @@ -72,16 +72,30 @@ export function {{classname}}FromJSONTyped(json: any, ignoreDiscriminator: boole
{{#items}}
{{#isDateType}}
if (Array.isArray(json)) {
{{#isDateLibraryTemporal}}
try {
return json.map(value => parseDate(value));
} catch {}
{{/isDateLibraryTemporal}}
{{^isDateLibraryTemporal}}
if (json.every(item => !(isNaN(parseDate(item).getTime())))) {
return json.map(value => parseDate(value));
}
{{/isDateLibraryTemporal}}
}
{{/isDateType}}
{{#isDateTimeType}}
if (Array.isArray(json)) {
{{#isDateLibraryTemporal}}
try {
return json.map(value => parseDateTime(value));
} catch {}
{{/isDateLibraryTemporal}}
{{^isDateLibraryTemporal}}
if (json.every(item => !(isNaN(parseDateTime(item).getTime())))) {
return json.map(value => parseDateTime(value));
}
{{/isDateLibraryTemporal}}
}
{{/isDateTimeType}}
{{#isNumeric}}
Expand Down Expand Up @@ -118,15 +132,29 @@ export function {{classname}}FromJSONTyped(json: any, ignoreDiscriminator: boole
{{#oneOfPrimitives}}
{{^isArray}}
{{#isDateType}}
{{#isDateLibraryTemporal}}
try {
return {{^required}}json == null ? undefined : {{/required}}({{#required}}{{#isNullable}}json == null ? null : {{/isNullable}}{{/required}}parseDate(json));
} catch {}
{{/isDateLibraryTemporal}}
{{^isDateLibraryTemporal}}
if (!(isNaN(parseDate(json).getTime()))) {
return {{^required}}json == null ? undefined : {{/required}}({{#required}}{{#isNullable}}json == null ? null : {{/isNullable}}{{/required}}parseDate(json));
}
{{/isDateLibraryTemporal}}
{{/isDateType}}
{{^isDateType}}
{{#isDateTimeType}}
{{#isDateLibraryTemporal}}
try {
return {{^required}}json == null ? undefined : {{/required}}({{#required}}{{#isNullable}}json == null ? null : {{/isNullable}}{{/required}}parseDateTime(json));
} catch {}
{{/isDateLibraryTemporal}}
{{^isDateLibraryTemporal}}
if (!(isNaN(parseDateTime(json).getTime()))) {
return {{^required}}json == null ? undefined : {{/required}}({{#required}}{{#isNullable}}json == null ? null : {{/isNullable}}{{/required}}parseDateTime(json));
}
{{/isDateLibraryTemporal}}
{{/isDateTimeType}}
{{/isDateType}}
{{#isNumeric}}
Expand Down Expand Up @@ -197,16 +225,30 @@ export function {{classname}}ToJSONTyped(value?: {{classname}} | null, ignoreDis
{{#items}}
{{#isDateType}}
if (Array.isArray(value)) {
{{#isDateLibraryTemporal}}
if (value.every(item => item instanceof Temporal.PlainDate)) {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
return value.map(value => serializeDate(value));
}
{{/isDateLibraryTemporal}}
{{^isDateLibraryTemporal}}
if (value.every(item => item instanceof Date)) {
return value.map(value => serializeDate(value));
}
{{/isDateLibraryTemporal}}
}
{{/isDateType}}
{{#isDateTimeType}}
if (Array.isArray(value)) {
{{#isDateLibraryTemporal}}
if (value.every(item => item instanceof Temporal.Instant)) {
return value.map(item => serializeDateTime(item));
}
{{/isDateLibraryTemporal}}
{{^isDateLibraryTemporal}}
if (value.every(item => item instanceof Date)) {
return value.map(item => serializeDateTime(item));
}
{{/isDateLibraryTemporal}}
}
{{/isDateTimeType}}
{{#isNumeric}}
Expand Down Expand Up @@ -243,14 +285,28 @@ export function {{classname}}ToJSONTyped(value?: {{classname}} | null, ignoreDis
{{#oneOfPrimitives}}
{{^isArray}}
{{#isDateType}}
{{#isDateLibraryTemporal}}
if (value instanceof Temporal.PlainDate) {
return (serializeDate(value{{#isNullable}} as any{{/isNullable}}));
}
{{/isDateLibraryTemporal}}
{{^isDateLibraryTemporal}}
if (value instanceof Date) {
return (serializeDate(value{{#isNullable}} as any{{/isNullable}}));
}
{{/isDateLibraryTemporal}}
{{/isDateType}}
{{#isDateTimeType}}
{{#isDateLibraryTemporal}}
if (value instanceof Temporal.Instant) {
return {{^required}}{{#isNullable}}value === null ? null : {{/isNullable}}{{^isNullable}}value == null ? undefined : {{/isNullable}}{{/required}}(serializeDateTime(value{{#isNullable}} as any{{/isNullable}}));
}
{{/isDateLibraryTemporal}}
{{^isDateLibraryTemporal}}
if (value instanceof Date) {
return {{^required}}{{#isNullable}}value === null ? null : {{/isNullable}}{{^isNullable}}value == null ? undefined : {{/isNullable}}{{/required}}(serializeDateTime(value{{#isNullable}} as any{{/isNullable}}));
}
{{/isDateLibraryTemporal}}
{{/isDateTimeType}}
{{#isNumeric}}
if (typeof value === 'number'{{#isEnum}} && ({{#allowableValues}}{{#values}}value === {{.}}{{^-last}} || {{/-last}}{{/values}}{{/allowableValues}}){{/isEnum}}) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,9 +350,19 @@ function querystringSingleKey(key: string, value: string | number | null | undef
const valueAsArray = Array.from(value);
return querystringSingleKey(key, valueAsArray, keyPrefix);
}
{{#isDateLibraryTemporal}}
if (value instanceof Temporal.Instant) {
return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`;
}
if (value instanceof Temporal.PlainDate) {
return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDate(value))}`;
}
{{/isDateLibraryTemporal}}
{{^isDateLibraryTemporal}}
if (value instanceof Date) {
return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`;
}
{{/isDateLibraryTemporal}}
if (value instanceof Object) {
return querystring(value as HTTPQuery, fullKey);
}
Expand All @@ -364,6 +374,30 @@ export function exists(json: any, key: string) {
return value !== null && value !== undefined;
}

{{#isDateLibraryTemporal}}
export function serializeDateTime(value: Temporal.Instant): string {
return value.toString();

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It's worth noting that Temporal.Instant's toString() method gives something like this (based on my experiments):

  • Node: "2026-08-15T14:25:49.161593018Z"
  • Firefox: "2026-08-15T14:25:46.876Z"
  • Chromium: "2026-08-15T14:26:24.1674Z"

So in Node, the precision is up to nanoseconds. Also, the Temporal specification says that the number of places after the decimal point may differ because trailing zeroes are removed.

Date's toISOString() method uses precision only down to milliseconds in all three runtimes I've tried.

Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
}

export function serializeDate(value: Temporal.PlainDate): string {
return value.toString({ calendarName: "never" });
}

export function parseDateTime(value: Temporal.Instant | string): Temporal.Instant {
if (value instanceof Temporal.Instant) {
return value;
}
return Temporal.Instant.from(value);
}

export function parseDate(value: Temporal.PlainDate | string): Temporal.PlainDate {
if (value instanceof Temporal.PlainDate) {
return value;
}
return Temporal.PlainDate.from(value);
}
{{/isDateLibraryTemporal}}
{{^isDateLibraryTemporal}}
/**
* Every generated date call site routes through these.
*
Expand Down Expand Up @@ -416,6 +450,7 @@ export function parseDateTime(value: any): Date {
return new Date(value);
}
{{/isDateLibraryDate}}
{{/isDateLibraryTemporal}}

{{^withoutRuntimeChecks}}
export function mapValues(data: any, fn: (item: any) => any) {
Expand Down
Loading
Loading