Skip to content

Commit f03470b

Browse files
Refactor root-level serialization logic and polymorphic value handling. (#72789)
* Refactor root-level serialization logic and polymorphic value handling. * Address feedback * Do not consult metadata LRU cache in JsonResumableConverter bridging logic. * Use secondary LRU cache when resolving root-level polymorphic types. * Add test coverage for root-level polymorphic values in JsonTypeInfo<T> JsonSerializer methods. * Change caching strategy for root-level polymorphic values. * Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs * Remove commented out code
1 parent c0d16fa commit f03470b

34 files changed

Lines changed: 443 additions & 387 deletions

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/CastingConverter.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ internal CastingConverter(JsonConverter<TSource> sourceConverter) : base(initial
2525
IsInternalConverterForNumberType = sourceConverter.IsInternalConverterForNumberType;
2626
RequiresReadAhead = sourceConverter.RequiresReadAhead;
2727
CanUseDirectReadOrWrite = sourceConverter.CanUseDirectReadOrWrite;
28+
CanBePolymorphic = sourceConverter.CanBePolymorphic;
2829
}
2930

3031
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
@@ -91,6 +92,11 @@ static void HandleFailure(TSource? source)
9192

9293
private static TSource CastOnWrite(T source)
9394
{
95+
if (default(TSource) is not null && default(T) is null && source is null)
96+
{
97+
ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(typeof(TSource));
98+
}
99+
94100
return (TSource)(object?)source!;
95101
}
96102
}

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,12 @@ internal override bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializer
7070
Debug.Assert(options == jsonTypeInfo.Options);
7171

7272
if (!state.SupportContinuation &&
73-
jsonTypeInfo.HasSerializeHandler &&
74-
jsonTypeInfo is JsonTypeInfo<T> info &&
75-
!state.CurrentContainsMetadata && // Do not use the fast path if state needs to write metadata.
76-
info.Options.SerializerContext?.CanUseSerializationLogic == true)
73+
jsonTypeInfo.CanUseSerializeHandler &&
74+
!state.CurrentContainsMetadata) // Do not use the fast path if state needs to write metadata.
7775
{
78-
Debug.Assert(info.SerializeHandler != null);
79-
info.SerializeHandler(writer, value);
76+
Debug.Assert(jsonTypeInfo is JsonTypeInfo<T> typeInfo && typeInfo.SerializeHandler != null);
77+
Debug.Assert(options.SerializerContext?.CanUseSerializationLogic == true);
78+
((JsonTypeInfo<T>)jsonTypeInfo).SerializeHandler!(writer, value);
8079
return true;
8180
}
8281

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.MetadataHandling.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,16 @@ public partial class JsonConverter
8080
case PolymorphicSerializationState.None:
8181
Debug.Assert(!state.IsContinuation);
8282

83+
if (state.IsPolymorphicRootValue && state.CurrentDepth == 0)
84+
{
85+
Debug.Assert(jsonTypeInfo.PolymorphicTypeResolver != null);
86+
87+
// We're serializing a root-level object value whose runtime type uses type hierarchies.
88+
// For consistency with nested value handling, we want to serialize as-is without emitting metadata.
89+
state.Current.PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryNotFound;
90+
break;
91+
}
92+
8393
Type runtimeType = value.GetType();
8494

8595
if (jsonTypeInfo.PolymorphicTypeResolver is PolymorphicTypeResolver resolver)

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.ReadCore.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public partial class JsonConverter<T>
2828
{
2929
if (state.SupportContinuation)
3030
{
31-
// If a Stream-based scenaio, return the actual value previously found;
31+
// If a Stream-based scenario, return the actual value previously found;
3232
// this may or may not be the final pass through here.
3333
state.BytesConsumed += reader.BytesConsumed;
3434
if (state.Current.ReturnValue == null)

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.WriteCore.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ internal sealed override bool WriteCoreAsObject(
1313
{
1414
if (
1515
#if NETCOREAPP
16-
// Short-circuit the check against "is not null"; treated as a constant by recent versions of the JIT.
16+
// Treated as a constant by recent versions of the JIT.
1717
typeof(T).IsValueType)
1818
#else
1919
IsValueType)
2020
#endif
2121
{
2222
// Value types can never have a null except for Nullable<T>.
23-
if (value == null && Nullable.GetUnderlyingType(TypeToConvert) == null)
23+
if (default(T) is not null && value is null)
2424
{
2525
ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert);
2626
}

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonResumableConverterOfT.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Text.Json.Serialization.Metadata;
5+
46
namespace System.Text.Json.Serialization
57
{
68
/// <summary>
@@ -20,7 +22,9 @@ internal abstract class JsonResumableConverter<T> : JsonConverter<T>
2022
// Bridge from resumable to value converters.
2123

2224
ReadStack state = default;
23-
state.Initialize(typeToConvert, options, supportContinuation: false);
25+
JsonTypeInfo jsonTypeInfo = options.GetTypeInfoInternal(typeToConvert);
26+
state.Initialize(jsonTypeInfo);
27+
2428
TryRead(ref reader, typeToConvert, options, ref state, out T? value);
2529
return value;
2630
}
@@ -33,9 +37,10 @@ public sealed override void Write(Utf8JsonWriter writer, T value, JsonSerializer
3337
}
3438

3539
// Bridge from resumable to value converters.
36-
3740
WriteStack state = default;
38-
state.Initialize(typeof(T), options, supportContinuation: false, supportAsync: false);
41+
JsonTypeInfo typeInfo = options.GetTypeInfoInternal(typeof(T));
42+
state.Initialize(typeInfo);
43+
3944
try
4045
{
4146
TryWrite(writer, value, options, ref state);

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,42 @@ public static partial class JsonSerializer
1515

1616
[RequiresUnreferencedCode(SerializationUnreferencedCodeMessage)]
1717
[RequiresDynamicCode(SerializationRequiresDynamicCodeMessage)]
18-
private static JsonTypeInfo GetTypeInfo(JsonSerializerOptions? options, Type runtimeType)
18+
private static JsonTypeInfo GetTypeInfo(JsonSerializerOptions? options, Type inputType)
1919
{
20-
Debug.Assert(runtimeType != null);
20+
Debug.Assert(inputType != null);
2121

2222
options ??= JsonSerializerOptions.Default;
2323

24-
if (!options.IsImmutable || !DefaultJsonTypeInfoResolver.IsDefaultInstanceRooted)
24+
if (!options.IsInitializedForReflectionSerializer)
2525
{
2626
options.InitializeForReflectionSerializer();
2727
}
2828

29-
return options.GetTypeInfoForRootType(runtimeType);
29+
// In order to improve performance of polymorphic root-level object serialization,
30+
// we bypass GetTypeInfoForRootType and cache JsonTypeInfo<object> in a dedicated property.
31+
// This lets any derived types take advantage of the cache in GetTypeInfoForRootType themselves.
32+
return inputType == JsonTypeInfo.ObjectType
33+
? options.ObjectTypeInfo
34+
: options.GetTypeInfoForRootType(inputType);
3035
}
3136

32-
private static JsonTypeInfo GetTypeInfo(JsonSerializerContext context, Type type)
37+
[RequiresUnreferencedCode(SerializationUnreferencedCodeMessage)]
38+
[RequiresDynamicCode(SerializationRequiresDynamicCodeMessage)]
39+
private static JsonTypeInfo<T> GetTypeInfo<T>(JsonSerializerOptions? options)
40+
=> (JsonTypeInfo<T>)GetTypeInfo(options, typeof(T));
41+
42+
private static JsonTypeInfo GetTypeInfo(JsonSerializerContext context, Type inputType)
3343
{
3444
Debug.Assert(context != null);
35-
Debug.Assert(type != null);
45+
Debug.Assert(inputType != null);
3646

37-
JsonTypeInfo? info = context.GetTypeInfo(type);
47+
JsonTypeInfo? info = context.GetTypeInfo(inputType);
3848
if (info is null)
3949
{
40-
ThrowHelper.ThrowInvalidOperationException_NoMetadataForType(type, context);
50+
ThrowHelper.ThrowInvalidOperationException_NoMetadataForType(inputType, context);
4151
}
4252

53+
info.EnsureConfigured();
4354
return info;
4455
}
4556

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Document.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ public static partial class JsonSerializer
105105
ThrowHelper.ThrowArgumentNullException(nameof(jsonTypeInfo));
106106
}
107107

108+
jsonTypeInfo.EnsureConfigured();
108109
return ReadDocument<TValue>(document, jsonTypeInfo);
109110
}
110111

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Element.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ public static partial class JsonSerializer
8585
ThrowHelper.ThrowArgumentNullException(nameof(jsonTypeInfo));
8686
}
8787

88+
jsonTypeInfo.EnsureConfigured();
8889
return ReadUsingMetadata<TValue>(element, jsonTypeInfo);
8990
}
9091

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,45 +9,45 @@ namespace System.Text.Json
99
{
1010
public static partial class JsonSerializer
1111
{
12-
private static TValue? ReadCore<TValue>(JsonConverter jsonConverter, ref Utf8JsonReader reader, JsonSerializerOptions options, ref ReadStack state)
12+
private static TValue? ReadCore<TValue>(ref Utf8JsonReader reader, JsonTypeInfo jsonTypeInfo, ref ReadStack state)
1313
{
14-
if (jsonConverter is JsonConverter<TValue> converter)
14+
if (jsonTypeInfo is JsonTypeInfo<TValue> typedInfo)
1515
{
1616
// Call the strongly-typed ReadCore that will not box structs.
17-
return converter.ReadCore(ref reader, options, ref state);
17+
return typedInfo.EffectiveConverter.ReadCore(ref reader, typedInfo.Options, ref state);
1818
}
1919

20-
// The non-generic API was called or we have a polymorphic case where TValue is not equal to the T in JsonConverter<T>.
21-
object? value = jsonConverter.ReadCoreAsObject(ref reader, options, ref state);
22-
Debug.Assert(value == null || value is TValue);
20+
// The non-generic API was called.
21+
object? value = jsonTypeInfo.Converter.ReadCoreAsObject(ref reader, jsonTypeInfo.Options, ref state);
22+
Debug.Assert(value is null or TValue);
2323
return (TValue?)value;
2424
}
2525

2626
private static TValue? ReadFromSpan<TValue>(ReadOnlySpan<byte> utf8Json, JsonTypeInfo jsonTypeInfo, int? actualByteCount = null)
2727
{
28+
Debug.Assert(jsonTypeInfo.IsConfigured);
29+
2830
JsonSerializerOptions options = jsonTypeInfo.Options;
2931

3032
var readerState = new JsonReaderState(options.GetReaderOptions());
3133
var reader = new Utf8JsonReader(utf8Json, isFinalBlock: true, readerState);
3234

3335
ReadStack state = default;
34-
jsonTypeInfo.EnsureConfigured();
3536
state.Initialize(jsonTypeInfo);
3637

3738
TValue? value;
38-
JsonConverter jsonConverter = jsonTypeInfo.Converter;
3939

4040
// For performance, the code below is a lifted ReadCore() above.
41-
if (jsonConverter is JsonConverter<TValue> converter)
41+
if (jsonTypeInfo is JsonTypeInfo<TValue> typedInfo)
4242
{
4343
// Call the strongly-typed ReadCore that will not box structs.
44-
value = converter.ReadCore(ref reader, options, ref state);
44+
value = typedInfo.EffectiveConverter.ReadCore(ref reader, options, ref state);
4545
}
4646
else
4747
{
48-
// The non-generic API was called or we have a polymorphic case where TValue is not equal to the T in JsonConverter<T>.
49-
object? objValue = jsonConverter.ReadCoreAsObject(ref reader, options, ref state);
50-
Debug.Assert(objValue == null || objValue is TValue);
48+
// The non-generic API was called.
49+
object? objValue = jsonTypeInfo.Converter.ReadCoreAsObject(ref reader, options, ref state);
50+
Debug.Assert(objValue is null or TValue);
5151
value = (TValue?)objValue;
5252
}
5353

0 commit comments

Comments
 (0)