#5405·NSwag

TypeScript client: `format: date` query and path parameters are serialised with `toISOString()` instead of a date-only string

Author: salahBouragbaCreated Sep 18, 2026Updated Sep 18, 2026

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

  • nswag npm 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 master templates as well: Client.RequestUrl.liquid and TypeScriptParameterModel.cs are unchanged there

Reproduction

spec.json:

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" } }
      }
    }
  }
}
bash
npx nswag openapi2tsclient /input:spec.json /output:client.ts /template:Angular /dateTimeType:Date

Generated:

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

typescript
        url_ += "startDate=" + encodeURIComponent(startDate ? formatDate(startDate) : "") + "&";

Root cause

  • Client.RequestUrl.liquid branches on parameter.IsDateOrDateTime (query and path loops alike) and emits parameter.GetDateTimeToString. Client.RequestBody.liquid does the same for form parameters.
  • TypeScriptParameterModel.GetDateTimeToString returns toISOString() for TypeScriptDateTimeType.Date without looking at the schema format. The only format it inspects is TimeSpan, for MomentJS and DayJS.
  • ParameterModelBase already exposes IsDate and IsDateTime separately (Schema.Format == JsonFormatStrings.Date versus DateTime), so the template has the information; it just never uses it.
  • NJsonSchema's ConvertToJavaScript.liquid does use IsDate and emits formatDate(...) (local getFullYear / getMonth / getDate, template File.FormatDate.liquid). TypeScriptGenerator.GenerateTypes only adds that utility artifact when a DTO artifact references formatDate(, so client code cannot rely on it being present today.

Proposed fix

  1. In Client.RequestUrl.liquid (both loops) and Client.RequestBody.liquid, add an {% elsif parameter.IsDate -%} branch before IsDateOrDateTime that emits formatDate(x) when the date type is Date (and the equivalent date-only formatting for MomentJS, DayJS and Luxon), leaving the IsDateTime case as it is.
  2. Emit the formatDate utility when the client code references it, not only when a DTO does, for example by having TypeScriptClientGenerator scan the client artifacts too, or by exposing RequiresFormatDateMethod on 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.