From 25893fb8e02052536724943429232620a9eabe8e Mon Sep 17 00:00:00 2001 From: Maksim Golev Date: Wed, 16 Apr 2025 14:14:04 +0400 Subject: [PATCH 1/3] feat(#113926): Adding a test to check for the presence of an bug. --- .../JsonNode/JsonValueTests.cs | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs index 29ae9662eedd4b..d1ed18167fdfe2 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; @@ -12,6 +13,116 @@ namespace System.Text.Json.Nodes.Tests { public static class JsonValueTests { + public static IEnumerable GetPrimitiveTypesTwoWaySerializationCases + { + get + { + return Enumerable.Empty(); +#if NET + //yield return new object[] + //{ + // JsonValue.Create(new DateOnly(2025, 4, 16)) + //}; + //yield return new object[] + //{ + // JsonValue.Create(Half.MaxValue) + //}; + //yield return new object[] + //{ + // JsonValue.Create(Int128.MaxValue) + //}; + //yield return new object[] + //{ + // JsonValue.Create(new TimeOnly(17, 18, 19)) + //}; + //yield return new object[] + //{ + // JsonValue.Create(UInt128.MaxValue) + //}; +#endif + //yield return new object[] + //{ + // JsonValue.Create(new DateTime(2025, 4, 16, 17, 18, 19)) + //}; + //yield return new object[] + //{ + // JsonValue.Create(new DateTimeOffset(2025, 4, 16, 17, 18, 19, new TimeSpan(10, 0, 0))) + //}; + //yield return new object[] + //{ + // JsonValue.Create(new Guid("CA79F1AC-AA0B-4704-8F7F-5A95DF0E4FD2")) + //}; + //yield return new object[] + //{ + // JsonValue.Create(new TimeSpan(1, 2, 3, 4, 5)) + //}; + //yield return new object[] + //{ + // JsonValue.Create(new Uri("http://contoso.com")) + //}; + //yield return new object[] + //{ + // JsonValue.Create(new Version(1, 2, 3, 4)) + //}; + //yield return new object[] + //{ + // JsonValue.Create(true) + //}; + //yield return new object[] + //{ + // JsonValue.Create(byte.MinValue) + //}; + //yield return new object[] + //{ + // JsonValue.Create('X') + //}; + //yield return new object[] + //{ + // JsonValue.Create(decimal.MinValue) + //}; + //yield return new object[] + //{ + // JsonValue.Create(double.MinValue) + //}; + //yield return new object[] + //{ + // JsonValue.Create(float.MaxValue) + //}; + //yield return new object[] + //{ + // JsonValue.Create(111) + //}; + //yield return new object[] + //{ + // JsonValue.Create(long.MaxValue) + //}; + //yield return new object[] + //{ + // JsonValue.Create(sbyte.MaxValue) + //}; + //yield return new object[] + //{ + // JsonValue.Create(short.MinValue) + //}; + //yield return new object[] + //{ + // JsonValue.Create("HelloWorld") + //}; + //yield return new object[] + //{ + // JsonValue.Create(uint.MaxValue) + //}; + //yield return new object[] + //{ + // JsonValue.Create(ulong.MaxValue) + //}; + //yield return new object[] + //{ + // JsonValue.Create(ushort.MaxValue) + //}; + } + } + [Fact] public static void CreateFromNull() { @@ -585,6 +696,19 @@ public static void PrimitiveTypes_EqualDeserializedValue(T value, JsonValueKi Assert.True(JsonNode.DeepEquals(clone, node)); } + [Theory] + [MemberData(nameof(GetPrimitiveTypesTwoWaySerializationCases))] + public static void PrimitiveTypes_TwoWaySerialization(JsonValue value) + { + string serialized = JsonSerializer.Serialize(value); + + object deserialized = JsonSerializer.Deserialize(serialized, value.GetType()); + + Assert.IsType(value.GetType(), deserialized); + + JsonNodeTests.AssertDeepEqual(value, deserialized as JsonNode); + } + public static IEnumerable GetPrimitiveTypes() { yield return Wrap(false, JsonValueKind.False); From 6f793ceac8950b4508aa0234244729ae2abfda9c Mon Sep 17 00:00:00 2001 From: Maksim Golev Date: Wed, 16 Apr 2025 14:51:48 +0400 Subject: [PATCH 2/3] fix(#113926): Correction of detected consequences of copy-paste. --- .../tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs index d1ed18167fdfe2..c6bc8da3b7b507 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs @@ -742,7 +742,7 @@ public static IEnumerable GetPrimitiveTypes() #if NET yield return Wrap(Half.MaxValue, JsonValueKind.Number); yield return Wrap((Int128)42, JsonValueKind.Number); - yield return Wrap((Int128)42, JsonValueKind.Number); + yield return Wrap((UInt128)42, JsonValueKind.Number); yield return Wrap((Memory)new byte[] { 1, 2, 3 }, JsonValueKind.String); yield return Wrap((ReadOnlyMemory)new byte[] { 1, 2, 3 }, JsonValueKind.String); yield return Wrap(new DateOnly(2024, 06, 20), JsonValueKind.String); From 9047de2e4492706e2d91c31706344d4a6886fe4f Mon Sep 17 00:00:00 2001 From: Maksim Golev Date: Wed, 23 Apr 2025 10:16:15 +0400 Subject: [PATCH 3/3] fix(#113926): Fix invalid deserialization JsonNode. --- .../src/System.Text.Json.csproj | 2 + .../src/System/ReflectionExtensions.cs | 9 + .../Node/JsonValuePrimitiveConverter.cs | 50 +++++ .../Node/JsonValuePrimitiveFactory.cs | 43 ++++ .../DefaultJsonTypeInfoResolver.Converters.cs | 1 + .../JsonNode/JsonValueTests.cs | 207 +++++++++--------- 6 files changed, 208 insertions(+), 104 deletions(-) create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValuePrimitiveConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValuePrimitiveFactory.cs diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 2d3b2af2e99d66..6447ea8338f660 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -126,6 +126,8 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + + diff --git a/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs b/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs index f2e9945b49c477..406cc0074726d6 100644 --- a/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs +++ b/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs @@ -6,12 +6,14 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace System.Text.Json.Reflection { internal static partial class ReflectionExtensions { + private static readonly Type s_jsonValuePrimitiveType = typeof(JsonValuePrimitive<>); private static readonly Type s_nullableType = typeof(Nullable<>); /// @@ -164,5 +166,12 @@ public static MemberInfo GetGenericMemberDefinition(this MemberInfo member) return member; } + + /// + /// Returns when the given type is of type . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsJsonValuePrimitiveOfT(this Type type) => + type.IsGenericType && type.GetGenericTypeDefinition() == s_jsonValuePrimitiveType; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValuePrimitiveConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValuePrimitiveConverter.cs new file mode 100644 index 00000000000000..56db9557212b46 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValuePrimitiveConverter.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using System.Text.Json.Schema; + +namespace System.Text.Json.Serialization.Converters.Node +{ + internal sealed class JsonValuePrimitiveConverter : JsonConverter?> + { + private readonly JsonConverter _elementConverter; + + public override bool HandleNull => true; + internal override bool CanPopulate => _elementConverter.CanPopulate; + internal override bool ConstructorIsParameterized => _elementConverter.ConstructorIsParameterized; + internal override Type? ElementType => typeof(JsonValuePrimitive); + internal override JsonConverter? NullableElementConverter => _elementConverter; + + public JsonValuePrimitiveConverter(JsonConverter elementConverter) + { + _elementConverter = elementConverter; + } + + public override JsonValuePrimitive? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + T value = _elementConverter.Read(ref reader, typeof(T), options)!; + JsonValuePrimitive returnValue = new JsonValuePrimitive(value, _elementConverter, null); + + return returnValue; + } + + public override void Write(Utf8JsonWriter writer, JsonValuePrimitive? value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + value.WriteTo(writer, options); + } + + internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) => _elementConverter.GetSchema(numberHandling); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValuePrimitiveFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValuePrimitiveFactory.cs new file mode 100644 index 00000000000000..644e13dd59b08c --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValuePrimitiveFactory.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Text.Json.Reflection; +using System.Text.Json.Serialization.Converters.Node; + +namespace System.Text.Json.Serialization.Converters +{ + [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] + internal sealed class JsonValuePrimitiveFactory : JsonConverterFactory + { + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsJsonValuePrimitiveOfT(); + } + + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + Debug.Assert(typeToConvert.IsJsonValuePrimitiveOfT()); + + Type valueTypeToConvert = typeToConvert.GetGenericArguments()[0]; + JsonConverter valueConverter = options.GetConverterInternal(valueTypeToConvert); + + return CreateValueConverter(valueTypeToConvert, valueConverter); + } + + public static JsonConverter CreateValueConverter(Type valueTypeToConvert, JsonConverter valueConverter) + { + return (JsonConverter)Activator.CreateInstance( + GetJsonValuePrimitiveConverterType(valueTypeToConvert), + BindingFlags.Instance | BindingFlags.Public, + binder: null, + args: new object[] { valueConverter }, + culture: null)!; + } + + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] + private static Type GetJsonValuePrimitiveConverterType(Type valueTypeToConvert) => typeof(JsonValuePrimitiveConverter<>).MakeGenericType(valueTypeToConvert); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Converters.cs index c96e2a1c8f5da5..bce3936da6cd51 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Converters.cs @@ -26,6 +26,7 @@ private static JsonConverterFactory[] GetDefaultFactoryConverters() // Nullable converter should always be next since it forwards to any nullable type. new NullableConverterFactory(), new EnumConverterFactory(), + new JsonValuePrimitiveFactory(), new JsonNodeConverterFactory(), new FSharpTypeConverterFactory(), new MemoryConverterFactory(), diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs index c6bc8da3b7b507..c5621345ba63c9 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; -using System.Linq; using System.Reflection; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; @@ -17,109 +16,108 @@ public static IEnumerable GetPrimitiveTypesTwoWaySerializationCases { get { - return Enumerable.Empty(); #if NET - //yield return new object[] - //{ - // JsonValue.Create(new DateOnly(2025, 4, 16)) - //}; - //yield return new object[] - //{ - // JsonValue.Create(Half.MaxValue) - //}; - //yield return new object[] - //{ - // JsonValue.Create(Int128.MaxValue) - //}; - //yield return new object[] - //{ - // JsonValue.Create(new TimeOnly(17, 18, 19)) - //}; - //yield return new object[] - //{ - // JsonValue.Create(UInt128.MaxValue) - //}; + yield return new object[] + { + JsonValue.Create(new DateOnly(2025, 4, 16)) + }; + yield return new object[] + { + JsonValue.Create(Half.MaxValue) + }; + yield return new object[] + { + JsonValue.Create(Int128.MaxValue) + }; + yield return new object[] + { + JsonValue.Create(new TimeOnly(17, 18, 19)) + }; + yield return new object[] + { + JsonValue.Create(UInt128.MaxValue) + }; #endif - //yield return new object[] - //{ - // JsonValue.Create(new DateTime(2025, 4, 16, 17, 18, 19)) - //}; - //yield return new object[] - //{ - // JsonValue.Create(new DateTimeOffset(2025, 4, 16, 17, 18, 19, new TimeSpan(10, 0, 0))) - //}; - //yield return new object[] - //{ - // JsonValue.Create(new Guid("CA79F1AC-AA0B-4704-8F7F-5A95DF0E4FD2")) - //}; - //yield return new object[] - //{ - // JsonValue.Create(new TimeSpan(1, 2, 3, 4, 5)) - //}; - //yield return new object[] - //{ - // JsonValue.Create(new Uri("http://contoso.com")) - //}; - //yield return new object[] - //{ - // JsonValue.Create(new Version(1, 2, 3, 4)) - //}; - //yield return new object[] - //{ - // JsonValue.Create(true) - //}; - //yield return new object[] - //{ - // JsonValue.Create(byte.MinValue) - //}; - //yield return new object[] - //{ - // JsonValue.Create('X') - //}; - //yield return new object[] - //{ - // JsonValue.Create(decimal.MinValue) - //}; - //yield return new object[] - //{ - // JsonValue.Create(double.MinValue) - //}; - //yield return new object[] - //{ - // JsonValue.Create(float.MaxValue) - //}; - //yield return new object[] - //{ - // JsonValue.Create(111) - //}; - //yield return new object[] - //{ - // JsonValue.Create(long.MaxValue) - //}; - //yield return new object[] - //{ - // JsonValue.Create(sbyte.MaxValue) - //}; - //yield return new object[] - //{ - // JsonValue.Create(short.MinValue) - //}; - //yield return new object[] - //{ - // JsonValue.Create("HelloWorld") - //}; - //yield return new object[] - //{ - // JsonValue.Create(uint.MaxValue) - //}; - //yield return new object[] - //{ - // JsonValue.Create(ulong.MaxValue) - //}; - //yield return new object[] - //{ - // JsonValue.Create(ushort.MaxValue) - //}; + yield return new object[] + { + JsonValue.Create(new DateTime(2025, 4, 16, 17, 18, 19)) + }; + yield return new object[] + { + JsonValue.Create(new DateTimeOffset(2025, 4, 16, 17, 18, 19, new TimeSpan(10, 0, 0))) + }; + yield return new object[] + { + JsonValue.Create(new Guid("CA79F1AC-AA0B-4704-8F7F-5A95DF0E4FD2")) + }; + yield return new object[] + { + JsonValue.Create(new TimeSpan(1, 2, 3, 4, 5)) + }; + yield return new object[] + { + JsonValue.Create(new Uri("http://contoso.com")) + }; + yield return new object[] + { + JsonValue.Create(new Version(1, 2, 3, 4)) + }; + yield return new object[] + { + JsonValue.Create(true) + }; + yield return new object[] + { + JsonValue.Create(byte.MinValue) + }; + yield return new object[] + { + JsonValue.Create('X') + }; + yield return new object[] + { + JsonValue.Create(decimal.MinValue) + }; + yield return new object[] + { + JsonValue.Create(double.MinValue) + }; + yield return new object[] + { + JsonValue.Create(float.MaxValue) + }; + yield return new object[] + { + JsonValue.Create(111) + }; + yield return new object[] + { + JsonValue.Create(long.MaxValue) + }; + yield return new object[] + { + JsonValue.Create(sbyte.MaxValue) + }; + yield return new object[] + { + JsonValue.Create(short.MinValue) + }; + yield return new object[] + { + JsonValue.Create("HelloWorld") + }; + yield return new object[] + { + JsonValue.Create(uint.MaxValue) + }; + yield return new object[] + { + JsonValue.Create(ulong.MaxValue) + }; + yield return new object[] + { + JsonValue.Create(ushort.MaxValue) + }; } } @@ -698,15 +696,16 @@ public static void PrimitiveTypes_EqualDeserializedValue(T value, JsonValueKi [Theory] [MemberData(nameof(GetPrimitiveTypesTwoWaySerializationCases))] - public static void PrimitiveTypes_TwoWaySerialization(JsonValue value) + public static void PrimitiveTypes_TwoWaySerialization(JsonValue value) { string serialized = JsonSerializer.Serialize(value); object deserialized = JsonSerializer.Deserialize(serialized, value.GetType()); + string serializedSecondTime = JsonSerializer.Serialize(deserialized); Assert.IsType(value.GetType(), deserialized); - JsonNodeTests.AssertDeepEqual(value, deserialized as JsonNode); + Assert.Equal(serialized, serializedSecondTime); } public static IEnumerable GetPrimitiveTypes()