Skip to content

[http-client-csharp] Fix duration integer encoding for non-Int32 wire types#10832

Merged
jorgerangel-msft merged 10 commits into
mainfrom
copilot/bugfix-duration-serialization-issue
May 29, 2026
Merged

[http-client-csharp] Fix duration integer encoding for non-Int32 wire types#10832
jorgerangel-msft merged 10 commits into
mainfrom
copilot/bugfix-duration-serialization-issue

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 28, 2026

A TypeSpec duration encoded as integer milliseconds/seconds with a wire type other than int32 (e.g. integer, int64, safeint, uint*) was serialized as a double via TimeSpan.TotalMilliseconds / TotalSeconds, producing fractional JSON output that violates the integer contract. Additionally, for wire types larger than int32 (e.g. int64, uint32, uint64, safeint, unbounded integer), using Int32-based ser/deser would overflow/truncate large values.

@encode(DurationKnownEncoding.milliseconds, integer)
audio_end_ms: duration;

Previously generated:

writer.WriteNumberValue(AudioEndMs.TotalMilliseconds); // double, may emit 123.45

Now generates (for int32-sized wire types):

writer.WriteNumberValue(Convert.ToInt32(Math.Round(AudioEndMs.TotalMilliseconds)));

And for wire types larger than int32 (int64, uint32, uint64, safeint, unbounded integer):

writer.WriteNumberValue(Convert.ToInt64(Math.Round(AudioEndMs.TotalMilliseconds)));

The Math.Round wrapper makes the rounding behavior explicit so fractional values (e.g. 1500.7ms) are rounded to the nearest integer (1501) rather than relying on Convert.ToInt32/ToInt64's implicit rounding.

Changes

  • TypeFactory.GetSerializationFormat: The DurationKnownEncoding.Seconds / Milliseconds switches only matched InputPrimitiveTypeKind.Int32 for the integer arm; every other integer kind fell through the _ default into Duration_*_Double. Extended both arms to cover the full integer-kind set, splitting based on .NET range:
    • Int8, Int16, Int32, UInt8, UInt16Duration_Seconds / Duration_Milliseconds (uses Int32).
    • Int64, UInt32, UInt64, SafeInt, unbounded Integer → new Duration_Seconds_Int64 / Duration_Milliseconds_Int64 (uses Int64).
    • Float / Float32 still map to _Float, and Float64 / others to _Double.
  • New SerializationFormat values: Added Duration_Seconds_Int64 and Duration_Milliseconds_Int64 to the input enum and to the generated SerializationFormat enum (SerializationFormatDefinition).
  • MrwSerializationTypeDefinition: Handles the new Int64 formats by emitting JsonElement.GetInt64() for deserialization and Convert.ToInt64(Math.Round(...)) for JSON serialization. The Int32 formats now also wrap in Math.Round for explicit rounding.
  • TypeFormattersDefinition: Handles the new Int64 formats in the URI/query-string ConvertToString path with Convert.ToInt64(Math.Round(...)).ToString(...). The Int32 formats now also wrap in Math.Round.
  • ConvertSnippets: Added InvokeToInt64 helper.
  • MathSnippets (new): Added InvokeRound helper that emits Math.Round(arg).
  • Tests:
    • TypeFactoryTests.DurationIntegerWireTypeSerializationFormat and DurationFloatWireTypeSerializationFormat cover every integer and float wire-type kind for both encodings, asserting Int32 vs Int64 routing.
    • MrwSerializationTypeDefinitionTests TestTimeSpanDeserializeExpression and TestTimeSpanSerializeStatement extended with the new Int64 format cases and the Math.Round wrapping.
    • JsonModelCoreTests.DurationMillisecondsIntegerWireTypeWritesAsInt / DurationMillisecondsFloatWireTypeWritesAsDouble and DeserializationTests.TestDeserializationOfDurationMillisecondsIntegerWireType build a model with a duration property and compare the full generated output against per-case TestData files (Int32 emits GetInt32/Convert.ToInt32(Math.Round(...)); Int64 and unbounded integer emit GetInt64/Convert.ToInt64(Math.Round(...))).

Validation

  • Full C# generator unit-test suites (Generator, Generator.Input, Generator.ClientModel, TestProjects.Local): all passing.
  • eng/scripts/Generate.ps1 ran to completion; the only regenerated changes are the expected updates to TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/SerializationFormat.cs and TypeFormatters.cs reflecting the new Int64 enum members, switch arms, and Math.Round wrapping.

Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
@microsoft-github-policy-service microsoft-github-policy-service Bot added the emitter:client:csharp Issue for the C# client emitter: @typespec/http-client-csharp label May 28, 2026
Copilot AI changed the title [WIP] Fix duration serialization issue with TimeSpan [http-client-csharp] Fix duration integer encoding for non-Int32 wire types May 28, 2026
Copilot AI requested a review from jorgerangel-msft May 28, 2026 19:41
Copy link
Copy Markdown
Contributor

@jorgerangel-msft jorgerangel-msft left a comment

Choose a reason for hiding this comment

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

@copilot lets add unit tests to validate the json deserialization + serialization of a model property specified with this encoding. Also rerun the generate.ps1 script and ensure all tests are passing.

Comment thread .chronus/changes/fix-csharp-duration-integer-encoding-2026-5-28-19-45-0.md Outdated
…on encoding

Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 28, 2026

@copilot lets add unit tests to validate the json deserialization + serialization of a model property specified with this encoding. Also rerun the generate.ps1 script and ensure all tests are passing.

Added in 4a84e15:

  • JsonModelCoreTests.DurationMillisecondsIntegerWireTypeWritesAsInt — builds a model with a duration property encoded as milliseconds for Int32, Int64, and integer wire types and asserts the generated JsonModelWriteCore emits writer.WriteNumberValue(global::System.Convert.ToInt32(AudioEndMs.TotalMilliseconds)) (and never the raw double).
  • JsonModelCoreTests.DurationMillisecondsFloatWireTypeWritesAsDouble — same for float32/float64 wire types, asserting the floating-point value is preserved.
  • DeserializationTests.TestDeserializationOfDurationMillisecondsIntegerWireType — asserts the generated deserialization uses TimeSpan.FromMilliseconds(prop.Value.GetInt32()) (never GetDouble()) for all integer wire types.

Ran eng/scripts/Generate.ps1 to completion; no regenerated files changed. Full generator test suite: 3073 passed, 0 failed.

Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 28, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/http-client-csharp@10832

commit: 19099cb

Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
@jorgerangel-msft
Copy link
Copy Markdown
Contributor

regen preview: Azure/azure-sdk-for-net#59519

Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes integer-encoded TypeSpec duration serialization in the C# emitter so wire types other than int32 (e.g. integer, int64, safeint, uint*) are emitted as integers rather than fractional doubles, and large values use int64 to avoid overflow. Adds explicit Math.Round wrapping so fractional milliseconds/seconds round predictably.

Changes:

  • Extend TypeFactory.GetSerializationFormat duration switches to cover all integer wire kinds, routing larger kinds to new Duration_Seconds_Int64 / Duration_Milliseconds_Int64 formats.
  • Add the new enum members to both the input SerializationFormat enum and the generated SerializationFormatDefinition, and handle them in MRW JSON ser/deser and TypeFormattersDefinition's ConvertToString; wrap existing Int32 paths in Math.Round.
  • Add ConvertSnippets.InvokeToInt64, new MathSnippets.InvokeRound, and broad TypeFactory / MRW / generated-model / deserialization tests; regenerate Sample-TypeSpec SerializationFormat.cs and TypeFormatters.cs.

Reviewed changes

Copilot reviewed 19 out of 21 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
Microsoft.TypeSpec.Generator/src/TypeFactory.cs Map full integer/long integer wire-kind sets to Int32 vs new Int64 duration formats.
Microsoft.TypeSpec.Generator/src/Snippets/MathSnippets.cs New helper emitting Math.Round(arg).
Microsoft.TypeSpec.Generator/src/Snippets/ConvertSnippets.cs Adds InvokeToInt64 helper.
Microsoft.TypeSpec.Generator.Input/src/InputTypes/Serialization/SerializationFormat.cs Adds Duration_Seconds_Int64 and Duration_Milliseconds_Int64 enum values.
Microsoft.TypeSpec.Generator.ClientModel/src/Providers/SerializationFormatDefinition.cs Emits the new Int64 enum members in the generated client enum.
Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs JSON ser/deser branches for new Int64 formats and Math.Round wrapping for Int32 formats.
Microsoft.TypeSpec.Generator.ClientModel/src/Providers/TypeFormattersDefinition.cs Adds Int64 TimeSpan switch cases and wraps Int32 cases in Math.Round.
Microsoft.TypeSpec.Generator/test/TypeFactoryTests.cs Parametrized tests for integer/float wire-kind routing.
Microsoft.TypeSpec.Generator.ClientModel/test/.../MrwSerializationTypeDefinitionTests.cs Adds Int64 ser/deser expression test cases and updated Math.Round expectations.
Microsoft.TypeSpec.Generator.ClientModel/test/.../JsonModelCoreTests.cs New tests asserting integer vs float duration model output by wire kind.
Microsoft.TypeSpec.Generator.ClientModel/test/.../DeserializationTests.cs New deserialization test for milliseconds integer wire kinds.
.../TestData/JsonModelCoreTests/DurationMillisecondsWireType.cs Expected per-wire-kind generated model write output.
.../TestData/DeserializationTests/TestDeserializationOfDurationMillisecondsIntegerWireType(*).cs Expected per-wire-kind generated deserialization output.
TestProjects/Local/Sample-TypeSpec/.../SerializationFormat.cs Regenerated enum with the new Int64 members.
TestProjects/Local/Sample-TypeSpec/.../TypeFormatters.cs Regenerated formatter cases for the new Int64 formats and Math.Round wrapping.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

No changes needing a change description found.

Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
@jorgerangel-msft jorgerangel-msft added this pull request to the merge queue May 29, 2026
Merged via the queue into main with commit 02cd17b May 29, 2026
29 checks passed
@jorgerangel-msft jorgerangel-msft deleted the copilot/bugfix-duration-serialization-issue branch May 29, 2026 19:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

emitter:client:csharp Issue for the C# client emitter: @typespec/http-client-csharp

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Duration encoded as integer (e.g. milliseconds) can serialize as floating point due to use of TimeSpan.TotalMilliseconds

4 participants