A custom string `format` silently degrades to `string` with no way to map it
Is your feature request related to a problem? Please describe.
A custom format on a string schema silently degrades the generated property to string, with no warning and no way to configure the mapping. That makes a custom format unusable in any document shared with an NSwag consumer, even when the format is meaningful to every other generator reading the same file.
CSharpTypeResolver.ResolveString (NJsonSchema, master) is a closed if-chain over JsonFormatStrings, ending in a catch-all:
if (schema.Format == JsonFormatStrings.Date) { return Settings.DateType …; }
if (schema.Format == JsonFormatStrings.DateTime) { return Settings.DateTimeType …; }
if (schema.Format == JsonFormatStrings.Time) { return Settings.TimeType …; }
// … Duration, TimeSpan, Uri, Guid/Uuid, Base64/Byte …
return "string" + nullableReferenceType; // ← any other formatCSharpGeneratorSettings exposes DateType, DateTimeType, TimeType, TimeSpanType, NumberType… — these let you change which type a known format maps to, but there is no way to add a key. TypeScriptTypeResolver.ResolveString has the same shape, so the limitation is symmetric.
The concrete case. OpenAPI describes a C# DateTime and a DateTimeOffset with the same format: "date-time", though they do not carry the same thing on the wire:
| C# | serialized |
|---|---|
DateTime (Kind = Unspecified) |
2024-01-31T00:00:00 |
DateTimeOffset |
2024-01-31T00:00:00+01:00 |
OpenAPI does not stand in the way of separating them: format is "an open string-valued property, and can have any value", so format: "date-time-offset" is a legal document. But measured on a real service (NSwag 14.7.0, dateTimeType: System.DateTimeOffset, one document generating both a TypeScript and a C# client):
| Document carries | C# client output |
|---|---|
format: "date-time" |
System.DateTimeOffset |
format: "date-time-offset" |
string — 45 properties degraded |
So the moment a producer emits the more precise format, every NSwag-generated .NET client loses its typing — silently, at build time, with no diagnostic. The failure is invisible until someone notices the client's properties became strings.
Worth noting the same collapse affects the generated C# client itself: with dateTimeType: System.DateTimeOffset, the 19 offset-less DateTime properties and the 45 DateTimeOffset ones all land on System.DateTimeOffset, so the client cannot distinguish them either. There is currently no way to express that difference in the document without breaking the client entirely.
Describe the solution you'd like
A configurable format → type map, so an unknown format is a decision rather than a silent fallback. Something like:
// nswag.json → codeGenerators.openApiToCSharpClient
"customTypeFormats": {
"date-time-offset": "System.DateTimeOffset",
"date-time-utc": "System.DateTime"
}consulted in ResolveString before the return "string" fallback. Unconfigured formats keep today's behaviour exactly, so this is additive and breaks nothing.
Two smaller variants, if a new setting is unwelcome:
- Make the fallback observable — log a warning when a non-empty
formaton astringschema matches no known format. That alone would have surfaced the problem at generation time instead of in review. - Make the resolution overridable —
ResolveStringisprivate;Resolveispublic override, so a subclass can technically intercept, but there is no documented way to inject a customCSharpTypeResolverthroughnswag.jsonor the CLI. A protected virtual hook plus a settings entry point would let consumers solve this themselves.
Describe alternatives you've considered
- Keeping the standard
formatand carrying the distinction in a vendor extension — what we ship today.format: "date-time"stays, and anx-temporal-format: "date-time-offset"rides alongside it; NSwag ignores unknownx-extensions and keeps emittingSystem.DateTimeOffset, while the other generator reads the extension. It works, andJsonSchema.ExtensionDatameans the information is already parsed and available should NSwag ever want to read it — but it means every producer has to know NSwag specifically, and pick a non-standard carrier for somethingformatwas designed to express. - Carrying it in
title— worse:titleis a display annotation, so the value surfaces as the property label in Swagger UI / Redoc. - Post-processing the generated client — rejected; the file is regenerated on every build.
- A
x-nswag-typestyle per-schema override — would solve it, but couples the document to one generator; a settings-side map keeps the document neutral.
Additional context
Happy to open a PR if the direction is agreeable — the change in ResolveString is small, and the symmetric one in TypeScriptTypeResolver would keep the two generators consistent. Please say which of the three shapes above you would accept before I write it.
Versions: NSwag 14.7.0 (NSwag.ApiDescription.Client, MSBuild target, .NET 10), document generated by Swashbuckle from an ASP.NET Core API.
Source: RicoSuter/NSwag