Skip to content

Bugfix/113926 fix invalid jsonnode deserialization #114863

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/libraries/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
<Compile Include="System\Text\Json\Serialization\Converters\Collection\RootLevelListConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Collection\StackOrQueueConverterWithReflection.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\JsonMetadataServicesConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Node\JsonValuePrimitiveConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Node\JsonValuePrimitiveFactory.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Object\ObjectWithParameterizedConstructorConverter.Large.Reflection.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Collection\MemoryConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Value\ReadOnlyMemoryByteConverter.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<>);

/// <summary>
Expand Down Expand Up @@ -164,5 +166,12 @@ public static MemberInfo GetGenericMemberDefinition(this MemberInfo member)

return member;
}

/// <summary>
/// Returns <see langword="true" /> when the given type is of type <see cref="JsonValuePrimitive{TValue}"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsJsonValuePrimitiveOfT(this Type type) =>
type.IsGenericType && type.GetGenericTypeDefinition() == s_jsonValuePrimitiveType;
}
}
Original file line number Diff line number Diff line change
@@ -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<T> : JsonConverter<JsonValuePrimitive<T>?>
{
private readonly JsonConverter<T> _elementConverter;

public override bool HandleNull => true;
internal override bool CanPopulate => _elementConverter.CanPopulate;
internal override bool ConstructorIsParameterized => _elementConverter.ConstructorIsParameterized;
internal override Type? ElementType => typeof(JsonValuePrimitive<T>);
internal override JsonConverter? NullableElementConverter => _elementConverter;

public JsonValuePrimitiveConverter(JsonConverter<T> elementConverter)
{
_elementConverter = elementConverter;
}

public override JsonValuePrimitive<T>? 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<T> returnValue = new JsonValuePrimitive<T>(value, _elementConverter, null);

return returnValue;
}

public override void Write(Utf8JsonWriter writer, JsonValuePrimitive<T>? value, JsonSerializerOptions options)
{
if (value is null)
{
writer.WriteNullValue();
return;
}

value.WriteTo(writer, options);
}

internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) => _elementConverter.GetSchema(numberHandling);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,115 @@ namespace System.Text.Json.Nodes.Tests
{
public static class JsonValueTests
{
public static IEnumerable<object[]> 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()
{
Expand Down Expand Up @@ -585,6 +694,20 @@ public static void PrimitiveTypes_EqualDeserializedValue<T>(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<object[]> GetPrimitiveTypes()
{
yield return Wrap(false, JsonValueKind.False);
Expand Down Expand Up @@ -618,7 +741,7 @@ public static IEnumerable<object[]> 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<byte>)new byte[] { 1, 2, 3 }, JsonValueKind.String);
yield return Wrap((ReadOnlyMemory<byte>)new byte[] { 1, 2, 3 }, JsonValueKind.String);
yield return Wrap(new DateOnly(2024, 06, 20), JsonValueKind.String);
Expand Down
Loading