diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs index 9cb3a3efc27fb0..1ac5814543c963 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs @@ -182,7 +182,27 @@ private static JsonSchema MapJsonSchemaCore( JsonTypeInfo elementTypeInfo = typeInfo.Options.GetTypeInfo(elementConverter.Type!); schema = MapJsonSchemaCore(ref state, elementTypeInfo, customConverter: elementConverter, cacheResult: false); - if (schema.Enum != null) + if (elementConverter.IsIeeeFloatingPointConverter && + (effectiveNumberHandling & JsonNumberHandling.AllowNamedFloatingPointLiterals) != 0) + { + // IEEE floating-point types with AllowNamedFloatingPointLiterals generate an anyOf schema. + // Fold null into the numeric branch to preserve nullability for nullable wrappers. + Debug.Assert(schema.AnyOf is not null, "IEEE floating-point types with AllowNamedFloatingPointLiterals should generate an anyOf schema."); + + List anyOf = schema.AnyOf; + Debug.Assert(anyOf.Exists(b => (b.Type & JsonSchemaType.Number) != 0), + "IEEE floating-point anyOf schema should have a numeric branch."); + + foreach (JsonSchema branch in anyOf) + { + if ((branch.Type & JsonSchemaType.Number) != 0) + { + branch.Type |= JsonSchemaType.Null; + break; + } + } + } + else if (schema.Enum != null) { Debug.Assert(elementTypeInfo.Type.IsEnum, "The enum keyword should only be populated by schemas for enum types."); schema.Enum.Add(null); // Append null to the enum array. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs index 0f45e0631a8222..a80e2427a86bbd 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs @@ -14,6 +14,8 @@ public DoubleConverter() IsInternalConverterForNumberType = true; } + internal override bool IsIeeeFloatingPointConverter => true; + public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (options?.NumberHandling is not null and not JsonNumberHandling.Strict) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs index a1eae208c3ddad..b94fb2ebb7c924 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs @@ -19,6 +19,8 @@ public HalfConverter() IsInternalConverterForNumberType = true; } + internal override bool IsIeeeFloatingPointConverter => true; + public override Half Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (options?.NumberHandling is not null and not JsonNumberHandling.Strict) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SingleConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SingleConverter.cs index 225fb08b892524..5cbdf5fbd80b45 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SingleConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SingleConverter.cs @@ -15,6 +15,8 @@ public SingleConverter() IsInternalConverterForNumberType = true; } + internal override bool IsIeeeFloatingPointConverter => true; + public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (options?.NumberHandling is not null and not JsonNumberHandling.Strict) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs index e2bd29a8dff101..8f8efccd306b5f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs @@ -175,6 +175,13 @@ internal JsonConverter CreateCastingConverter() /// internal bool IsValueType { get; init; } + /// + /// Indicates whether this converter handles IEEE 754 floating-point types + /// (double, float, Half) that may emit anyOf schemas with named floating-point + /// literals under AllowNamedFloatingPointLiterals. + /// + internal virtual bool IsIeeeFloatingPointConverter => false; + /// /// Whether the converter is built-in. /// diff --git a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs index c4c14ef1c1b0f8..065f18e501d5ec 100644 --- a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs +++ b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs @@ -285,6 +285,73 @@ public static IEnumerable GetTestDataCore() } """); + // Regression test for https://github.com/dotnet/runtime/issues/129432 + // Nullable floating-point types under AllowNamedFloatingPointLiterals must retain the null branch. + yield return new TestData( + Value: 3.14, + AdditionalValues: [null, double.NaN, double.PositiveInfinity, double.NegativeInfinity], + ExpectedJsonSchema: """ + { + "anyOf": [ + { "type": ["number", "null"] }, + { "enum": ["NaN", "Infinity", "-Infinity"] } + ] + } + """, + SerializerOptions: new() { NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals }); + + yield return new TestData( + Value: 1.2f, + AdditionalValues: [null, float.NaN, float.PositiveInfinity, float.NegativeInfinity], + ExpectedJsonSchema: """ + { + "anyOf": [ + { "type": ["number", "null"] }, + { "enum": ["NaN", "Infinity", "-Infinity"] } + ] + } + """, + SerializerOptions: new() { NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals }); + +#if NET + yield return new TestData( + Value: (Half)1.5, + AdditionalValues: [null, Half.NaN, Half.PositiveInfinity, Half.NegativeInfinity], + ExpectedJsonSchema: """ + { + "anyOf": [ + { "type": ["number", "null"] }, + { "enum": ["NaN", "Infinity", "-Infinity"] } + ] + } + """, + SerializerOptions: new() { NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals }); +#endif + + yield return new TestData( + Value: new() { Latitude = 3.14, Longitude = 1.2f }, + AdditionalValues: [new() { Latitude = null, Longitude = null }], + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "Latitude": { + "anyOf": [ + { "type": ["number", "null"] }, + { "enum": ["NaN", "Infinity", "-Infinity"] } + ] + }, + "Longitude": { + "anyOf": [ + { "type": ["number", "null"] }, + { "enum": ["NaN", "Infinity", "-Infinity"] } + ] + } + } + } + """, + SerializerOptions: new() { NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals }); + yield return new TestData( Value: new() { Value = 1, Next = new() { Value = 2, Next = new() { Value = 3 } } }, AdditionalValues: [new() { Value = 1, Next = null }], @@ -1290,6 +1357,12 @@ public class PocoWithCustomNumberHandlingOnProperties public decimal DecimalAllowingFloatingPointLiteralsAndReadingFromString { get; set; } } + public class PocoWithNullableFloatingPoint + { + public double? Latitude { get; set; } + public float? Longitude { get; set; } + } + public class PocoWithRecursiveMembers { public int Value { get; init; } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSchemaExporterTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSchemaExporterTests.cs index 01558bb3658642..f4cf65cb00a8b9 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSchemaExporterTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSchemaExporterTests.cs @@ -72,6 +72,10 @@ public sealed partial class JsonSchemaExporterTests_SourceGen() [JsonSerializable(typeof(bool?))] [JsonSerializable(typeof(int?))] [JsonSerializable(typeof(double?))] + [JsonSerializable(typeof(float?))] +#if NET + [JsonSerializable(typeof(Half?))] +#endif [JsonSerializable(typeof(Guid?))] [JsonSerializable(typeof(JsonElement?))] [JsonSerializable(typeof(IntEnum?))] @@ -87,6 +91,7 @@ public sealed partial class JsonSchemaExporterTests_SourceGen() [JsonSerializable(typeof(PocoWithCustomNaming))] [JsonSerializable(typeof(PocoWithCustomNumberHandling))] [JsonSerializable(typeof(PocoWithCustomNumberHandlingOnProperties))] + [JsonSerializable(typeof(PocoWithNullableFloatingPoint))] [JsonSerializable(typeof(PocoWithRecursiveMembers))] [JsonSerializable(typeof(PocoWithRecursiveCollectionElement))] [JsonSerializable(typeof(PocoWithRecursiveDictionaryValue))]