Skip to content

Commit

Permalink
Refactor root-level serialization logic and polymorphic value handlin…
Browse files Browse the repository at this point in the history
…g. (#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
  • Loading branch information
eiriktsarpalis authored Jul 29, 2022
1 parent c0d16fa commit f03470b
Show file tree
Hide file tree
Showing 34 changed files with 443 additions and 387 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ internal CastingConverter(JsonConverter<TSource> sourceConverter) : base(initial
IsInternalConverterForNumberType = sourceConverter.IsInternalConverterForNumberType;
RequiresReadAhead = sourceConverter.RequiresReadAhead;
CanUseDirectReadOrWrite = sourceConverter.CanUseDirectReadOrWrite;
CanBePolymorphic = sourceConverter.CanBePolymorphic;
}

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

private static TSource CastOnWrite(T source)
{
if (default(TSource) is not null && default(T) is null && source is null)
{
ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(typeof(TSource));
}

return (TSource)(object?)source!;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,12 @@ internal override bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializer
Debug.Assert(options == jsonTypeInfo.Options);

if (!state.SupportContinuation &&
jsonTypeInfo.HasSerializeHandler &&
jsonTypeInfo is JsonTypeInfo<T> info &&
!state.CurrentContainsMetadata && // Do not use the fast path if state needs to write metadata.
info.Options.SerializerContext?.CanUseSerializationLogic == true)
jsonTypeInfo.CanUseSerializeHandler &&
!state.CurrentContainsMetadata) // Do not use the fast path if state needs to write metadata.
{
Debug.Assert(info.SerializeHandler != null);
info.SerializeHandler(writer, value);
Debug.Assert(jsonTypeInfo is JsonTypeInfo<T> typeInfo && typeInfo.SerializeHandler != null);
Debug.Assert(options.SerializerContext?.CanUseSerializationLogic == true);
((JsonTypeInfo<T>)jsonTypeInfo).SerializeHandler!(writer, value);
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@ public partial class JsonConverter
case PolymorphicSerializationState.None:
Debug.Assert(!state.IsContinuation);

if (state.IsPolymorphicRootValue && state.CurrentDepth == 0)
{
Debug.Assert(jsonTypeInfo.PolymorphicTypeResolver != null);

// We're serializing a root-level object value whose runtime type uses type hierarchies.
// For consistency with nested value handling, we want to serialize as-is without emitting metadata.
state.Current.PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryNotFound;
break;
}

Type runtimeType = value.GetType();

if (jsonTypeInfo.PolymorphicTypeResolver is PolymorphicTypeResolver resolver)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public partial class JsonConverter<T>
{
if (state.SupportContinuation)
{
// If a Stream-based scenaio, return the actual value previously found;
// If a Stream-based scenario, return the actual value previously found;
// this may or may not be the final pass through here.
state.BytesConsumed += reader.BytesConsumed;
if (state.Current.ReturnValue == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ internal sealed override bool WriteCoreAsObject(
{
if (
#if NETCOREAPP
// Short-circuit the check against "is not null"; treated as a constant by recent versions of the JIT.
// Treated as a constant by recent versions of the JIT.
typeof(T).IsValueType)
#else
IsValueType)
#endif
{
// Value types can never have a null except for Nullable<T>.
if (value == null && Nullable.GetUnderlyingType(TypeToConvert) == null)
if (default(T) is not null && value is null)
{
ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// 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.Serialization.Metadata;

namespace System.Text.Json.Serialization
{
/// <summary>
Expand All @@ -20,7 +22,9 @@ internal abstract class JsonResumableConverter<T> : JsonConverter<T>
// Bridge from resumable to value converters.

ReadStack state = default;
state.Initialize(typeToConvert, options, supportContinuation: false);
JsonTypeInfo jsonTypeInfo = options.GetTypeInfoInternal(typeToConvert);
state.Initialize(jsonTypeInfo);

TryRead(ref reader, typeToConvert, options, ref state, out T? value);
return value;
}
Expand All @@ -33,9 +37,10 @@ public sealed override void Write(Utf8JsonWriter writer, T value, JsonSerializer
}

// Bridge from resumable to value converters.

WriteStack state = default;
state.Initialize(typeof(T), options, supportContinuation: false, supportAsync: false);
JsonTypeInfo typeInfo = options.GetTypeInfoInternal(typeof(T));
state.Initialize(typeInfo);

try
{
TryWrite(writer, value, options, ref state);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,42 @@ public static partial class JsonSerializer

[RequiresUnreferencedCode(SerializationUnreferencedCodeMessage)]
[RequiresDynamicCode(SerializationRequiresDynamicCodeMessage)]
private static JsonTypeInfo GetTypeInfo(JsonSerializerOptions? options, Type runtimeType)
private static JsonTypeInfo GetTypeInfo(JsonSerializerOptions? options, Type inputType)
{
Debug.Assert(runtimeType != null);
Debug.Assert(inputType != null);

options ??= JsonSerializerOptions.Default;

if (!options.IsImmutable || !DefaultJsonTypeInfoResolver.IsDefaultInstanceRooted)
if (!options.IsInitializedForReflectionSerializer)
{
options.InitializeForReflectionSerializer();
}

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

private static JsonTypeInfo GetTypeInfo(JsonSerializerContext context, Type type)
[RequiresUnreferencedCode(SerializationUnreferencedCodeMessage)]
[RequiresDynamicCode(SerializationRequiresDynamicCodeMessage)]
private static JsonTypeInfo<T> GetTypeInfo<T>(JsonSerializerOptions? options)
=> (JsonTypeInfo<T>)GetTypeInfo(options, typeof(T));

private static JsonTypeInfo GetTypeInfo(JsonSerializerContext context, Type inputType)
{
Debug.Assert(context != null);
Debug.Assert(type != null);
Debug.Assert(inputType != null);

JsonTypeInfo? info = context.GetTypeInfo(type);
JsonTypeInfo? info = context.GetTypeInfo(inputType);
if (info is null)
{
ThrowHelper.ThrowInvalidOperationException_NoMetadataForType(type, context);
ThrowHelper.ThrowInvalidOperationException_NoMetadataForType(inputType, context);
}

info.EnsureConfigured();
return info;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ public static partial class JsonSerializer
ThrowHelper.ThrowArgumentNullException(nameof(jsonTypeInfo));
}

jsonTypeInfo.EnsureConfigured();
return ReadDocument<TValue>(document, jsonTypeInfo);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ public static partial class JsonSerializer
ThrowHelper.ThrowArgumentNullException(nameof(jsonTypeInfo));
}

jsonTypeInfo.EnsureConfigured();
return ReadUsingMetadata<TValue>(element, jsonTypeInfo);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,45 +9,45 @@ namespace System.Text.Json
{
public static partial class JsonSerializer
{
private static TValue? ReadCore<TValue>(JsonConverter jsonConverter, ref Utf8JsonReader reader, JsonSerializerOptions options, ref ReadStack state)
private static TValue? ReadCore<TValue>(ref Utf8JsonReader reader, JsonTypeInfo jsonTypeInfo, ref ReadStack state)
{
if (jsonConverter is JsonConverter<TValue> converter)
if (jsonTypeInfo is JsonTypeInfo<TValue> typedInfo)
{
// Call the strongly-typed ReadCore that will not box structs.
return converter.ReadCore(ref reader, options, ref state);
return typedInfo.EffectiveConverter.ReadCore(ref reader, typedInfo.Options, ref state);
}

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

private static TValue? ReadFromSpan<TValue>(ReadOnlySpan<byte> utf8Json, JsonTypeInfo jsonTypeInfo, int? actualByteCount = null)
{
Debug.Assert(jsonTypeInfo.IsConfigured);

JsonSerializerOptions options = jsonTypeInfo.Options;

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

ReadStack state = default;
jsonTypeInfo.EnsureConfigured();
state.Initialize(jsonTypeInfo);

TValue? value;
JsonConverter jsonConverter = jsonTypeInfo.Converter;

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public static partial class JsonSerializer
ThrowHelper.ThrowArgumentNullException(nameof(jsonTypeInfo));
}

jsonTypeInfo.EnsureConfigured();
return ReadNode<TValue>(node, jsonTypeInfo);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public static partial class JsonSerializer
ThrowHelper.ThrowArgumentNullException(nameof(jsonTypeInfo));
}

jsonTypeInfo.EnsureConfigured();
return ReadFromSpan<TValue>(utf8Json, jsonTypeInfo);
}

Expand Down
Loading

0 comments on commit f03470b

Please sign in to comment.