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 29ae9662eedd4b..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 @@ -12,6 +12,115 @@ namespace System.Text.Json.Nodes.Tests { public static class JsonValueTests { + public static IEnumerable GetPrimitiveTypesTwoWaySerializationCases + { + get + { +#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 +694,20 @@ 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()); + string serializedSecondTime = JsonSerializer.Serialize(deserialized); + + Assert.IsType(value.GetType(), deserialized); + JsonNodeTests.AssertDeepEqual(value, deserialized as JsonNode); + Assert.Equal(serialized, serializedSecondTime); + } + public static IEnumerable GetPrimitiveTypes() { yield return Wrap(false, JsonValueKind.False); @@ -618,7 +741,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);