From f3a3c4930205e6c04262ddd5f02c12b9fcb8f114 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Nov 2025 23:21:12 +0000 Subject: [PATCH 1/2] Initial plan From a9fbe0aafa5b3eba48b80a8d786b295c2f628a42 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Nov 2025 23:29:50 +0000 Subject: [PATCH 2/2] Implement full JsonNumberHandling support Co-authored-by: Havret <9103861+Havret@users.noreply.github.com> --- .../InternalConverters/FieldConverter.cs | 41 ++++- .../JsonNumberHandlingTests.cs | 157 ++++++++++++++++++ 2 files changed, 195 insertions(+), 3 deletions(-) diff --git a/src/Protobuf.System.Text.Json/InternalConverters/FieldConverter.cs b/src/Protobuf.System.Text.Json/InternalConverters/FieldConverter.cs index 8ded5c2..b415f68 100644 --- a/src/Protobuf.System.Text.Json/InternalConverters/FieldConverter.cs +++ b/src/Protobuf.System.Text.Json/InternalConverters/FieldConverter.cs @@ -11,6 +11,7 @@ internal class FieldConverter : InternalConverter { private JsonConverter? _converter; private readonly bool _isConverterForNumberType; + private readonly bool _isConverterForFloatingPointType; public FieldConverter() { @@ -18,12 +19,21 @@ public FieldConverter() type = Nullable.GetUnderlyingType(type) ?? type; var typeCode = Type.GetTypeCode(type); _isConverterForNumberType = typeCode is >= TypeCode.SByte and <= TypeCode.Decimal; + _isConverterForFloatingPointType = typeCode is TypeCode.Single or TypeCode.Double; } public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { _converter ??= GetConverter(ref options); - _converter.Write(writer, (T) value, options); + + if (_isConverterForNumberType && (JsonNumberHandling.WriteAsString & options.NumberHandling) != 0) + { + writer.WriteStringValue(value.ToString()); + } + else + { + _converter.Write(writer, (T) value, options); + } } public override void Read(ref Utf8JsonReader reader, IMessage obj, Type typeToConvert, JsonSerializerOptions options, @@ -37,8 +47,33 @@ public override void Read(ref Utf8JsonReader reader, IMessage obj, Type typeToCo if (_isConverterForNumberType && reader.TokenType == JsonTokenType.String && (JsonNumberHandling.AllowReadingFromString & options.NumberHandling) != 0) { - var value = Convert.ChangeType(reader.GetString(), typeToConvert); - fieldAccessor.SetValue(obj, value); + var stringValue = reader.GetString(); + + // Check if it's a named floating-point literal + if (_isConverterForFloatingPointType && (stringValue == "NaN" || stringValue == "Infinity" || stringValue == "-Infinity")) + { + // Only allow these if the flag is set + if ((JsonNumberHandling.AllowNamedFloatingPointLiterals & options.NumberHandling) != 0) + { + var value = Convert.ChangeType(stringValue switch + { + "NaN" => typeToConvert == typeof(float) ? float.NaN : double.NaN, + "Infinity" => typeToConvert == typeof(float) ? float.PositiveInfinity : double.PositiveInfinity, + "-Infinity" => typeToConvert == typeof(float) ? float.NegativeInfinity : double.NegativeInfinity, + _ => throw new JsonException($"Unexpected floating-point literal: {stringValue}") + }, typeToConvert); + fieldAccessor.SetValue(obj, value); + return; + } + else + { + // Flag not set, so this should fail + throw new JsonException($"The string '{stringValue}' cannot be converted to a number without JsonNumberHandling.AllowNamedFloatingPointLiterals."); + } + } + + var convertedValue = Convert.ChangeType(stringValue, typeToConvert); + fieldAccessor.SetValue(obj, convertedValue); } else { diff --git a/test/Protobuf.System.Text.Json.Tests/JsonNumberHandlingTests.cs b/test/Protobuf.System.Text.Json.Tests/JsonNumberHandlingTests.cs index bc653a9..637fc53 100644 --- a/test/Protobuf.System.Text.Json.Tests/JsonNumberHandlingTests.cs +++ b/test/Protobuf.System.Text.Json.Tests/JsonNumberHandlingTests.cs @@ -1,3 +1,4 @@ +using System; using System.Text.Json; using System.Text.Json.Protobuf.Tests; using System.Text.Json.Serialization; @@ -81,4 +82,160 @@ public void Should_fail_deserializing_numbers_as_strings_when_NumberHandling_not // Act & Assert Assert.Throws(() => JsonSerializer.Deserialize(json, options)); } + + [Fact] + public void Should_serialize_numbers_as_strings_when_NumberHandling_set_to_WriteAsString() + { + // Arrange + var message = new SimpleMessage + { + DoubleProperty = 0.12d, + FloatProperty = 7.89f, + Int32Property = 123, + Int64Property = 456L, + Uint32Property = 789u, + Uint64Property = 101112u, // Note: this is actually uint32 in proto + Sint32Property = 1314, + Sint64Property = 151617L, + Fixed32Property = 1819u, + Fixed64Property = 202122ul, + Sfixed32Property = 2324, + Sfixed64Property = 252627L + }; + + var options = TestHelper.CreateJsonSerializerOptions(); + options.NumberHandling = JsonNumberHandling.WriteAsString; + + // Act + var json = JsonSerializer.Serialize(message, options); + + // Assert + Assert.Contains("\"doubleProperty\":\"0.12\"", json); + Assert.Contains("\"floatProperty\":\"7.89\"", json); + Assert.Contains("\"int32Property\":\"123\"", json); + Assert.Contains("\"int64Property\":\"456\"", json); + Assert.Contains("\"uint32Property\":\"789\"", json); + Assert.Contains("\"uint64Property\":\"101112\"", json); + Assert.Contains("\"sint32Property\":\"1314\"", json); + Assert.Contains("\"sint64Property\":\"151617\"", json); + Assert.Contains("\"fixed32Property\":\"1819\"", json); + Assert.Contains("\"fixed64Property\":\"202122\"", json); + Assert.Contains("\"sfixed32Property\":\"2324\"", json); + Assert.Contains("\"sfixed64Property\":\"252627\"", json); + } + + [Fact] + public void Should_not_serialize_numbers_as_strings_when_NumberHandling_set_to_Strict() + { + // Arrange + var message = new SimpleMessage + { + DoubleProperty = 0.12d, + FloatProperty = 7.89f, + Int32Property = 123 + }; + + var options = TestHelper.CreateJsonSerializerOptions(); + options.NumberHandling = JsonNumberHandling.Strict; + + // Act + var json = JsonSerializer.Serialize(message, options); + + // Assert - numbers should be JSON numbers, not strings + Assert.Contains("\"doubleProperty\":0.12", json); + Assert.Contains("\"floatProperty\":7.89", json); + Assert.Contains("\"int32Property\":123", json); + } + + [Fact] + public void Should_deserialize_named_floating_point_literals_when_AllowNamedFloatingPointLiterals_flag_set() + { + // Arrange + var json = + """ + { + "doubleProperty": "NaN", + "floatProperty": "Infinity" + } + """; + + var options = TestHelper.CreateJsonSerializerOptions(); + options.NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals; + + // Act + var deserialized = JsonSerializer.Deserialize(json, options); + + // Assert + Assert.NotNull(deserialized); + Assert.True(double.IsNaN(deserialized.DoubleProperty)); + Assert.True(float.IsPositiveInfinity(deserialized.FloatProperty)); + } + + [Fact] + public void Should_deserialize_negative_infinity_when_AllowNamedFloatingPointLiterals_flag_set() + { + // Arrange + var json = + """ + { + "doubleProperty": "-Infinity", + "floatProperty": "-Infinity" + } + """; + + var options = TestHelper.CreateJsonSerializerOptions(); + options.NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals; + + // Act + var deserialized = JsonSerializer.Deserialize(json, options); + + // Assert + Assert.NotNull(deserialized); + Assert.True(double.IsNegativeInfinity(deserialized.DoubleProperty)); + Assert.True(float.IsNegativeInfinity(deserialized.FloatProperty)); + } + + [Fact] + public void Should_fail_deserializing_named_floating_point_literals_when_flag_not_set() + { + // Arrange + var json = + """ + { + "doubleProperty": "NaN" + } + """; + + var options = TestHelper.CreateJsonSerializerOptions(); + // Only AllowReadingFromString, but not AllowNamedFloatingPointLiterals + options.NumberHandling = JsonNumberHandling.AllowReadingFromString; + + // Act & Assert + Assert.Throws(() => JsonSerializer.Deserialize(json, options)); + } + + [Fact] + public void Should_round_trip_with_WriteAsString_and_AllowReadingFromString() + { + // Arrange + var message = new SimpleMessage + { + DoubleProperty = 123.456d, + FloatProperty = 78.9f, + Int32Property = 999 + }; + + var options = TestHelper.CreateJsonSerializerOptions(); + options.NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString; + + // Act + var json = JsonSerializer.Serialize(message, options); + var deserialized = JsonSerializer.Deserialize(json, options); + + // Assert + Assert.NotNull(deserialized); + Assert.Equal(message.DoubleProperty, deserialized.DoubleProperty); + Assert.Equal(message.FloatProperty, deserialized.FloatProperty); + Assert.Equal(message.Int32Property, deserialized.Int32Property); + } } \ No newline at end of file