Skip to content
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

Fix a number of issues related to boxed value serialization #84768

Merged
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,29 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, object? va
runtimeConverter.WriteAsPropertyNameCoreAsObject(writer, value, options, isWritingExtensionDataProperty);
}
}

/// <summary>
/// A placeholder ObjectConverter used for driving object root value
/// serialization only and does not root JsonNode/JsonDocument.
/// </summary>
internal sealed class ObjectConverterSlim : JsonConverter<object?>
{
private protected override ConverterStrategy GetDefaultConverterStrategy() => ConverterStrategy.Object;

public ObjectConverterSlim()
{
CanBePolymorphic = true;
}

public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
Debug.Fail("Converter should only be used to drive root-level object serialization.");
return null;
}

public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options)
{
Debug.Fail("Converter should only be used to drive root-level object serialization.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,21 +80,18 @@ public partial class JsonConverter
case PolymorphicSerializationState.None:
Debug.Assert(!state.IsContinuation);

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

// 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;
if (CanBePolymorphic && runtimeType != TypeToConvert)
{
Debug.Assert(TypeToConvert == typeof(object));
jsonTypeInfo = state.Current.InitializePolymorphicReEntry(runtimeType, options);
polymorphicConverter = jsonTypeInfo.Converter;
}

Type runtimeType = value.GetType();

if (jsonTypeInfo.PolymorphicTypeResolver is PolymorphicTypeResolver resolver)
{
Debug.Assert(CanHaveMetadata);
Debug.Assert(jsonTypeInfo.Converter.CanHaveMetadata);

if (resolver.TryGetDerivedJsonTypeInfo(runtimeType, out JsonTypeInfo? derivedJsonTypeInfo, out object? typeDiscriminator))
{
Expand All @@ -108,26 +105,16 @@ public partial class JsonConverter
}

state.PolymorphicTypeDiscriminator = typeDiscriminator;
state.PolymorphicTypeResolver = resolver;
}
}
else
{
state.Current.PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryNotFound;
}
}
else
{
Debug.Assert(CanBePolymorphic);

if (runtimeType != TypeToConvert)
{
polymorphicConverter = state.Current.InitializePolymorphicReEntry(runtimeType, options);
}
else
{
state.Current.PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryNotFound;
}
if (polymorphicConverter is null)
{
state.Current.PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryNotFound;
}

break;

case PolymorphicSerializationState.PolymorphicReEntrySuspended:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali
{
// Special case object converters since they don't
// require the expensive ReadStack.Push()/Pop() operations.
Debug.Assert(this is ObjectConverter);
Debug.Assert(this is ObjectConverter or ObjectConverterSlim);
success = OnTryRead(ref reader, typeToConvert, options, ref state, out value);
Debug.Assert(success);
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public static partial class JsonSerializer

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

Expand All @@ -45,7 +45,7 @@ private static JsonTypeInfo GetTypeInfo(JsonSerializerOptions? options, Type inp
// This lets any derived types take advantage of the cache in GetTypeInfoForRootType themselves.
return inputType == JsonTypeInfo.ObjectType
? options.ObjectTypeInfo
: options.GetTypeInfoForRootType(inputType, fallBackToNearestAncestorType);
: options.GetTypeInfoForRootType(inputType);
}

[RequiresUnreferencedCode(SerializationUnreferencedCodeMessage)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public static byte[] SerializeToUtf8Bytes(
JsonSerializerOptions? options = null)
{
ValidateInputType(value, inputType);
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType);
return WriteBytesAsObject(value, jsonTypeInfo);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public static JsonDocument SerializeToDocument<TValue>(TValue value, JsonSeriali
public static JsonDocument SerializeToDocument(object? value, Type inputType, JsonSerializerOptions? options = null)
{
ValidateInputType(value, inputType);
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType);
return WriteDocumentAsObject(value, jsonTypeInfo);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public static JsonElement SerializeToElement<TValue>(TValue value, JsonSerialize
public static JsonElement SerializeToElement(object? value, Type inputType, JsonSerializerOptions? options = null)
{
ValidateInputType(value, inputType);
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType);
return WriteElementAsObject(value, jsonTypeInfo);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ internal static MetadataPropertyName WriteMetadataForObject(

if (state.PolymorphicTypeDiscriminator is object discriminator)
{
Debug.Assert(state.Parent.JsonPropertyInfo!.JsonTypeInfo.PolymorphicTypeResolver != null);
Debug.Assert(state.PolymorphicTypeResolver != null);

JsonEncodedText propertyName =
state.Parent.JsonPropertyInfo.JsonTypeInfo.PolymorphicTypeResolver.CustomTypeDiscriminatorPropertyNameJsonEncoded is JsonEncodedText customPropertyName
state.PolymorphicTypeResolver.CustomTypeDiscriminatorPropertyNameJsonEncoded is JsonEncodedText customPropertyName
? customPropertyName
: s_metadataType;

Expand Down Expand Up @@ -88,7 +88,9 @@ internal static bool TryGetReferenceForValue(object currentValue, ref WriteStack
writer.WriteString(s_metadataRef, referenceId);
writer.WriteEndObject();

state.PolymorphicTypeDiscriminator = null; // clear out any polymorphism state.
// clear out any polymorphism state.
state.PolymorphicTypeDiscriminator = null;
state.PolymorphicTypeResolver = null;
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public static partial class JsonSerializer
public static JsonNode? SerializeToNode(object? value, Type inputType, JsonSerializerOptions? options = null)
{
ValidateInputType(value, inputType);
JsonTypeInfo typeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
JsonTypeInfo typeInfo = GetTypeInfo(options, inputType);
return WriteNodeAsObject(value, typeInfo);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ public static Task SerializeAsync(
}

ValidateInputType(value, inputType);
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType);
return jsonTypeInfo.SerializeAsObjectAsync(utf8Json, value, cancellationToken);
}

Expand Down Expand Up @@ -152,7 +152,7 @@ public static void Serialize(
}

ValidateInputType(value, inputType);
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType);
jsonTypeInfo.SerializeAsObject(utf8Json, value);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public static string Serialize(
JsonSerializerOptions? options = null)
{
ValidateInputType(value, inputType);
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType);
return WriteStringAsObject(value, jsonTypeInfo);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public static void Serialize(
}

ValidateInputType(value, inputType);
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType);
jsonTypeInfo.SerializeAsObject(writer, value);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Converters;
using System.Text.Json.Serialization.Metadata;

namespace System.Text.Json
Expand Down Expand Up @@ -161,7 +162,14 @@ internal bool TryGetPolymorphicTypeInfoForRootType(object rootValue, [NotNullWhe
Type runtimeType = rootValue.GetType();
if (runtimeType != JsonTypeInfo.ObjectType)
{
// To determine the contract for an object value:
// 1. Find the JsonTypeInfo for the runtime type with fallback to the nearest ancestor, if not available.
// 2. If the resolved type is deriving from a polymorphic type, use the contract of the polymorphic type instead.
polymorphicTypeInfo = GetTypeInfoForRootType(runtimeType, fallBackToNearestAncestorType: true);
if (polymorphicTypeInfo.AncestorPolymorphicType is { } ancestorPolymorphicType)
{
polymorphicTypeInfo = ancestorPolymorphicType;
}
return true;
}

Expand All @@ -175,7 +183,22 @@ internal JsonTypeInfo ObjectTypeInfo
get
{
Debug.Assert(IsReadOnly);
return _objectTypeInfo ??= GetTypeInfoInternal(JsonTypeInfo.ObjectType);
return _objectTypeInfo ??= GetObjectTypeInfo(this);

static JsonTypeInfo GetObjectTypeInfo(JsonSerializerOptions options)
{
JsonTypeInfo? typeInfo = options.GetTypeInfoInternal(JsonTypeInfo.ObjectType, ensureNotNull: null);
if (typeInfo is null)
{
// If the user-supplied resolver does not provide a JsonTypeInfo<object>,
// use a placeholder value to drive root-level boxed value serialization.
var converter = new ObjectConverterSlim();
typeInfo = new JsonTypeInfo<object>(converter, options);
typeInfo.EnsureConfigured();
}

return typeInfo;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,30 @@ private void Configure()
}
}

/// <summary>
/// Gets any ancestor polymorphic types that declare
/// a type discriminator for the current type. Consulted
/// when serializing polymorphic values as objects.
/// </summary>
internal JsonTypeInfo? AncestorPolymorphicType
{
get
{
Debug.Assert(IsConfigured);

if (!_isAncestorPolymorphicTypeResolved)
{
_ancestorPolymorhicType = PolymorphicTypeResolver.FindNearestPolymorphicBaseType(this);
_isAncestorPolymorphicTypeResolved = true;
}

return _ancestorPolymorhicType;
}
}

private JsonTypeInfo? _ancestorPolymorhicType;
private volatile bool _isAncestorPolymorphicTypeResolved;

/// <summary>
/// Determines if the transitive closure of all JsonTypeInfo metadata referenced
/// by the current type (property types, key types, element types, ...) are
Expand Down Expand Up @@ -877,9 +901,9 @@ public JsonPropertyInfo CreateJsonPropertyInfo(Type propertyType, string name)
internal JsonParameterInfoValues[]? ParameterInfoValues { get; set; }

// Untyped, root-level serialization methods
internal abstract void SerializeAsObject(Utf8JsonWriter writer, object? rootValue, bool isInvokedByPolymorphicConverter = false);
internal abstract Task SerializeAsObjectAsync(Stream utf8Json, object? rootValue, CancellationToken cancellationToken, bool isInvokedByPolymorphicConverter = false);
internal abstract void SerializeAsObject(Stream utf8Json, object? rootValue, bool isInvokedByPolymorphicConverter = false);
internal abstract void SerializeAsObject(Utf8JsonWriter writer, object? rootValue);
internal abstract Task SerializeAsObjectAsync(Stream utf8Json, object? rootValue, CancellationToken cancellationToken);
internal abstract void SerializeAsObject(Stream utf8Json, object? rootValue);

// Untyped, root-level deserialization methods
internal abstract object? DeserializeAsObject(ref Utf8JsonReader reader, ref ReadStack state);
Expand Down Expand Up @@ -1248,7 +1272,7 @@ private static JsonTypeInfoKind GetTypeInfoKind(Type type, JsonConverter convert
if (type == typeof(object) && converter.CanBePolymorphic)
{
// System.Object is polymorphic and will not respect Properties
Debug.Assert(converter is ObjectConverter);
Debug.Assert(converter is ObjectConverter or ObjectConverterSlim);
return JsonTypeInfoKind.None;
}

Expand Down
Loading