TypeScript client: `format: date` query and path parameters are serialised with `toISOString()` instead of a date-only string
Summary
In the TypeScript client, a query or path parameter whose schema is { "type": "string", "format": "date" } is typed Date (correct) but serialised with toISOString(), so the request carries a full instant such as 2026-09-17T07:55:42.262Z where the contract says a date-only string. Servers that bind a date-only type reject it: ASP.NET Core DateOnly answers an automatic 400 before the action runs, and NodaTime LocalDate or Java LocalDate behave the same. DTO body properties with format: date are handled correctly through NJsonSchema's formatDate(...), so the same schema behaves differently depending on where it appears.
This is the same defect as #2479 (2019) and, for form parameters, #4295. I am opening a fresh report because the root cause is now precisely known and the fix is small.
Version
nswagnpm package 14.7.1 (NSwag.Commands / NSwag.CodeGeneration.TypeScript 14.7.1, NJsonSchema 11.6.1), also NSwag.MSBuild 14.7.1- Template
Angular,dateTimeType: Date - Reproduced against the
mastertemplates as well:Client.RequestUrl.liquidandTypeScriptParameterModel.csare unchanged there
Reproduction
spec.json:
{
"openapi": "3.0.0",
"info": { "title": "Probe", "version": "1" },
"paths": {
"/stats": {
"get": {
"operationId": "Stats_Get",
"parameters": [
{ "name": "startDate", "in": "query", "schema": { "type": "string", "format": "date", "nullable": true } }
],
"responses": { "200": { "description": "ok" } }
}
}
}
}npx nswag openapi2tsclient /input:spec.json /output:client.ts /template:Angular /dateTimeType:DateGenerated:
get(startDate: Date | null | undefined): Observable<void> {
let url_ = this.baseUrl + "/stats?";
if (startDate !== undefined && startDate !== null)
url_ += "startDate=" + encodeURIComponent(startDate ? "" + startDate.toISOString() : "") + "&";Expected, consistent with what NJsonSchema already emits for a format: date DTO property:
url_ += "startDate=" + encodeURIComponent(startDate ? formatDate(startDate) : "") + "&";Root cause
Client.RequestUrl.liquidbranches onparameter.IsDateOrDateTime(query and path loops alike) and emitsparameter.GetDateTimeToString.Client.RequestBody.liquiddoes the same for form parameters.TypeScriptParameterModel.GetDateTimeToStringreturnstoISOString()forTypeScriptDateTimeType.Datewithout looking at the schema format. The only format it inspects isTimeSpan, for MomentJS and DayJS.ParameterModelBasealready exposesIsDateandIsDateTimeseparately (Schema.Format == JsonFormatStrings.DateversusDateTime), so the template has the information; it just never uses it.- NJsonSchema's
ConvertToJavaScript.liquiddoes useIsDateand emitsformatDate(...)(localgetFullYear/getMonth/getDate, templateFile.FormatDate.liquid).TypeScriptGenerator.GenerateTypesonly adds that utility artifact when a DTO artifact referencesformatDate(, so client code cannot rely on it being present today.
Proposed fix
- In
Client.RequestUrl.liquid(both loops) andClient.RequestBody.liquid, add an{% elsif parameter.IsDate -%}branch beforeIsDateOrDateTimethat emitsformatDate(x)when the date type isDate(and the equivalent date-only formatting for MomentJS, DayJS and Luxon), leaving theIsDateTimecase as it is. - Emit the
formatDateutility when the client code references it, not only when a DTO does, for example by havingTypeScriptClientGeneratorscan the client artifacts too, or by exposingRequiresFormatDateMethodon the file model.
Alternatively make GetDateTimeToString format-aware, but that property returns a method-call suffix, which does not fit a free function like formatDate, so the template branch seems cleaner.
I am happy to submit a pull request along these lines if that helps.
Workaround
A templateDirectory override of Client.RequestUrl.liquid with the IsDate branch inlined works with 14.7.1, at the cost of carrying a copy of the template. That is what we shipped after this took down the statistics widgets of a production dashboard: the OpenAPI document said format: date the whole time, so the contract looked right from both sides until the server started binding DateOnly.
Source: RicoSuter/NSwag