Skip to content

[csharp][generichost] Improve date deserialization - #25022

Draft
devhl-labs wants to merge 2 commits into
OpenAPITools:masterfrom
devhl-labs:devhl/improve-date-deserialization
Draft

devhl-labs wants to merge 2 commits into
OpenAPITools:masterfrom
devhl-labs:devhl/improve-date-deserialization

Conversation

@devhl-labs

@devhl-labs devhl-labs commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Normalize JSON date and date-time deserialization in the C# Generic Host client and ensure invalid non-null values fail consistently with JsonException.

Previously, nullable converters could silently return null for malformed values, making an invalid server response indistinguishable from an actual JSON null. Parsing was also limited to a fixed set of exact formats, rejecting valid RFC 3339 timestamps with more than seven fractional digits.

Behavior

  • JSON null remains valid for nullable date and date-time properties.
  • Invalid strings and incorrect JSON token types now throw JsonException for nullable and non-nullable properties.
  • Token-type errors and malformed-string errors have distinct diagnostic messages.
  • Standard timezone-qualified date-time values are parsed through System.Text.Json and normalized to UTC.
  • RFC 3339 timestamps with extended fractional seconds are accepted.
  • Numeric offsets are normalized to the corresponding UTC instant.
  • Offset-less date-times are rejected by default because OpenAPI date-time follows RFC 3339, which requires an offset.
  • Default compatibility formats now accept only valid OpenAPI representations:
    • date-time: RFC 3339 values with Z or a numeric offset.
    • date: yyyy-MM-dd.
  • Nonstandard compact formats are no longer accepted by default.

The customizable format partial remains available for users who intentionally need to permit compatibility formats, including offset-less values. Such values are interpreted as UTC through the existing fallback parsing policy.

Nullable converters now handle only JSON null themselves. Non-null values are delegated through the configured JsonSerializerOptions, ensuring nullable and non-nullable properties use the same converter behavior without bypassing the configured serialization pipeline.

PR checklist

  • Read the contribution guidelines.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Summary by cubic

Improves the C# generichost date and date-time JSON converters so invalid values throw a descriptive JsonException instead of a generic NotSupportedException or silently returning null. Generated samples are updated.

Behavior changes

  • Non-string tokens and unparseable values now throw JsonException with a clear message instead of NotSupportedException or returning null.
  • Date-times with an offset now parse via System.Text.Json, and the supported format lists were trimmed to drop the compact yyyyMMdd variants; offset-less ISO values fall back to the custom formats.

Written for commit dcc02cc. Summary will update on new commits.

Review in cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment •

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.

All reported issues were addressed across 169 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

6 issues found across 171 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="samples/client/petstore/csharp/generichost/net9/OneOf/src/Org.OpenAPITools/Client/DateTimeNullableJsonConverter.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net9/OneOf/src/Org.OpenAPITools/Client/DateTimeNullableJsonConverter.cs:33">
P2: `JsonSerializer.Deserialize<DateTime>` removes the converter's UTC normalization: offset-less values become `Unspecified` and offset values become local. Preserve `AdjustToUniversal | AssumeUniversal` so downstream timestamp handling does not reinterpret API values.</violation>

<violation number="2" location="samples/client/petstore/csharp/generichost/net9/OneOf/src/Org.OpenAPITools/Client/DateTimeNullableJsonConverter.cs:47">
P2: This delegation changes the serialized wire format from fixed seven-digit fractions to trimmed fractions, such as `.0000000Z` becoming `Z`. Preserve the explicit format unless this breaking payload change is intentional and documented.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/latest/OneOfList/src/Org.OpenAPITools/Client/DateTimeNullableJsonConverter.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/latest/OneOfList/src/Org.OpenAPITools/Client/DateTimeNullableJsonConverter.cs:33">
P1: This delegation rejects the compact `yyyyMMddTHHmmss...` timestamps that the converter previously accepted. Preserve those formats before falling back to the built-in parser, or explicitly document this breaking wire-format change.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/net9/Petstore/src/Org.OpenAPITools/Client/DateTimeJsonConverter.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net9/Petstore/src/Org.OpenAPITools/Client/DateTimeJsonConverter.cs:47">
P2: This rejects values that the converter itself serializes: `Write` emits no timezone suffix for `DateTimeKind.Unspecified`, while this branch rejects the resulting offset-less timestamp and the fallback array is empty. Preserve the existing offset-less compatibility path or serialize unspecified values with an explicit UTC offset so converter round-trips do not throw.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/net4.7/Petstore/src/Org.OpenAPITools/Client/DateTimeJsonConverter.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net4.7/Petstore/src/Org.OpenAPITools/Client/DateTimeJsonConverter.cs:48">
P3: `string value = reader.GetString()` runs before the `TryGetDateTime` fast path even though the standard RFC 3339-with-offset case returns immediately without using `value`. Move it below the fast-path check so the common path avoids decoding and allocating the string. `TryGetDateTime` operates on the current token without advancing the reader, so the reorder is behavior-neutral.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/net10/Petstore/src/Org.OpenAPITools/Client/DateTimeNullableJsonConverter.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net10/Petstore/src/Org.OpenAPITools/Client/DateTimeNullableJsonConverter.cs:33">
P3: The nullable converters now delegate `Read`/`Write` to `JsonSerializer.Deserialize<DateTime>(ref reader, options)` / `Serialize(writer, dateTimeValue.Value, options)`, so their parsing and output behavior is fully determined by whether the non-nullable converter happens to be registered in `options`. This works in the generated clients (both converter types are registered together), but the removed public `Formats` on the nullable converters means users who customize the JSON options and register only the nullable converters silently lose the format-based fallback and get plain ISO-only parsing. Instantiate the non-nullable converter explicitly (e.g. `new DateTimeJsonConverter().Read(...)`) so the nullable path no longer depends on the options registry.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

return result;

return null;
return JsonSerializer.Deserialize<DateTime>(ref reader, options);

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.

P1: This delegation rejects the compact yyyyMMddTHHmmss... timestamps that the converter previously accepted. Preserve those formats before falling back to the built-in parser, or explicitly document this breaking wire-format change.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/csharp/generichost/latest/OneOfList/src/Org.OpenAPITools/Client/DateTimeNullableJsonConverter.cs, line 33:

<comment>This delegation rejects the compact `yyyyMMddTHHmmss...` timestamps that the converter previously accepted. Preserve those formats before falling back to the built-in parser, or explicitly document this breaking wire-format change.</comment>

<file context>
@@ -54,16 +30,7 @@ public class DateTimeNullableJsonConverter : JsonConverter<DateTime?>
-                    return result;
-
-            throw new JsonException("The JSON value is not a valid date-time.");
+            return JsonSerializer.Deserialize<DateTime>(ref reader, options);
         }
 
</file context>

writer.WriteNullValue();
else
writer.WriteStringValue(dateTimeValue.Value.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffK", CultureInfo.InvariantCulture));
JsonSerializer.Serialize(writer, dateTimeValue.Value, options);

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: This delegation changes the serialized wire format from fixed seven-digit fractions to trimmed fractions, such as .0000000Z becoming Z. Preserve the explicit format unless this breaking payload change is intentional and documented.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/csharp/generichost/net9/OneOf/src/Org.OpenAPITools/Client/DateTimeNullableJsonConverter.cs, line 47:

<comment>This delegation changes the serialized wire format from fixed seven-digit fractions to trimmed fractions, such as `.0000000Z` becoming `Z`. Preserve the explicit format unless this breaking payload change is intentional and documented.</comment>

<file context>
@@ -77,7 +44,7 @@ public override void Write(Utf8JsonWriter writer, DateTime? dateTimeValue, JsonS
                 writer.WriteNullValue();
             else
-                writer.WriteStringValue(dateTimeValue.Value.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffK", CultureInfo.InvariantCulture));
+                JsonSerializer.Serialize(writer, dateTimeValue.Value, options);
         }
     }
</file context>
Suggested change
JsonSerializer.Serialize(writer, dateTimeValue.Value, options);
writer.WriteStringValue(dateTimeValue.Value.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffK", System.Globalization.CultureInfo.InvariantCulture));

return result;

return null;
return JsonSerializer.Deserialize<DateTime>(ref reader, options);

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: JsonSerializer.Deserialize<DateTime> removes the converter's UTC normalization: offset-less values become Unspecified and offset values become local. Preserve AdjustToUniversal | AssumeUniversal so downstream timestamp handling does not reinterpret API values.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/csharp/generichost/net9/OneOf/src/Org.OpenAPITools/Client/DateTimeNullableJsonConverter.cs, line 33:

<comment>`JsonSerializer.Deserialize<DateTime>` removes the converter's UTC normalization: offset-less values become `Unspecified` and offset values become local. Preserve `AdjustToUniversal | AssumeUniversal` so downstream timestamp handling does not reinterpret API values.</comment>

<file context>
@@ -54,16 +30,7 @@ public class DateTimeNullableJsonConverter : JsonConverter<DateTime?>
-                    return result;
-
-            throw new JsonException("The JSON value is not a valid date-time.");
+            return JsonSerializer.Deserialize<DateTime>(ref reader, options);
         }
 
</file context>

// System.Text.Json also accepts offset-less ISO 8601 values, but OpenAPI
// date-time values follow RFC 3339, which requires an offset. Let those
// non-standard values fall through so custom formats can explicitly permit them.
if (reader.TryGetDateTime(out DateTime dateTime) && dateTime.Kind != DateTimeKind.Unspecified)

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: This rejects values that the converter itself serializes: Write emits no timezone suffix for DateTimeKind.Unspecified, while this branch rejects the resulting offset-less timestamp and the fallback array is empty. Preserve the existing offset-less compatibility path or serialize unspecified values with an explicit UTC offset so converter round-trips do not throw.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/csharp/generichost/net9/Petstore/src/Org.OpenAPITools/Client/DateTimeJsonConverter.cs, line 47:

<comment>This rejects values that the converter itself serializes: `Write` emits no timezone suffix for `DateTimeKind.Unspecified`, while this branch rejects the resulting offset-less timestamp and the fallback array is empty. Preserve the existing offset-less compatibility path or serialize unspecified values with an explicit UTC offset so converter round-trips do not throw.</comment>

<file context>
@@ -52,15 +36,24 @@ public class DateTimeJsonConverter : JsonConverter<DateTime>
+            // System.Text.Json also accepts offset-less ISO 8601 values, but OpenAPI
+            // date-time values follow RFC 3339, which requires an offset. Let those
+            // non-standard values fall through so custom formats can explicitly permit them.
+            if (reader.TryGetDateTime(out DateTime dateTime) && dateTime.Kind != DateTimeKind.Unspecified)
+                return dateTime.ToUniversalTime();
+
</file context>

// System.Text.Json also accepts offset-less ISO 8601 values, but OpenAPI
// date-time values follow RFC 3339, which requires an offset. Let those
// non-standard values fall through so custom formats can explicitly permit them.
if (reader.TryGetDateTime(out DateTime dateTime) && dateTime.Kind != DateTimeKind.Unspecified)

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.

P3: string value = reader.GetString() runs before the TryGetDateTime fast path even though the standard RFC 3339-with-offset case returns immediately without using value. Move it below the fast-path check so the common path avoids decoding and allocating the string. TryGetDateTime operates on the current token without advancing the reader, so the reorder is behavior-neutral.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/csharp/generichost/net4.7/Petstore/src/Org.OpenAPITools/Client/DateTimeJsonConverter.cs, line 48:

<comment>`string value = reader.GetString()` runs before the `TryGetDateTime` fast path even though the standard RFC 3339-with-offset case returns immediately without using `value`. Move it below the fast-path check so the common path avoids decoding and allocating the string. `TryGetDateTime` operates on the current token without advancing the reader, so the reorder is behavior-neutral.</comment>

<file context>
@@ -54,15 +37,24 @@ public class DateTimeJsonConverter : JsonConverter<DateTime>
+            // System.Text.Json also accepts offset-less ISO 8601 values, but OpenAPI
+            // date-time values follow RFC 3339, which requires an offset. Let those
+            // non-standard values fall through so custom formats can explicitly permit them.
+            if (reader.TryGetDateTime(out DateTime dateTime) && dateTime.Kind != DateTimeKind.Unspecified)
+                return dateTime.ToUniversalTime();
+
</file context>

return result;

return null;
return JsonSerializer.Deserialize<DateTime>(ref reader, options);

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.

P3: The nullable converters now delegate Read/Write to JsonSerializer.Deserialize<DateTime>(ref reader, options) / Serialize(writer, dateTimeValue.Value, options), so their parsing and output behavior is fully determined by whether the non-nullable converter happens to be registered in options. This works in the generated clients (both converter types are registered together), but the removed public Formats on the nullable converters means users who customize the JSON options and register only the nullable converters silently lose the format-based fallback and get plain ISO-only parsing. Instantiate the non-nullable converter explicitly (e.g. new DateTimeJsonConverter().Read(...)) so the nullable path no longer depends on the options registry.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/csharp/generichost/net10/Petstore/src/Org.OpenAPITools/Client/DateTimeNullableJsonConverter.cs, line 33:

<comment>The nullable converters now delegate `Read`/`Write` to `JsonSerializer.Deserialize<DateTime>(ref reader, options)` / `Serialize(writer, dateTimeValue.Value, options)`, so their parsing and output behavior is fully determined by whether the non-nullable converter happens to be registered in `options`. This works in the generated clients (both converter types are registered together), but the removed public `Formats` on the nullable converters means users who customize the JSON options and register only the nullable converters silently lose the format-based fallback and get plain ISO-only parsing. Instantiate the non-nullable converter explicitly (e.g. `new DateTimeJsonConverter().Read(...)`) so the nullable path no longer depends on the options registry.</comment>

<file context>
@@ -54,16 +30,7 @@ public class DateTimeNullableJsonConverter : JsonConverter<DateTime?>
-                    return result;
-
-            throw new JsonException("The JSON value is not a valid date-time.");
+            return JsonSerializer.Deserialize<DateTime>(ref reader, options);
         }
 
</file context>

@devhl-labs
devhl-labs marked this pull request as draft September 27, 2026 23:06

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant