Skip to content

Commit 3a01050

Browse files
Fix a number of issues related to boxed value serialization (#84768)
* Remove unspeakable type support form serialization overloads accepting an explicit `Type` parameter. * Honor polymorphism metadata when serializing boxed polymorphic values. * Support boxed root value serialization in source generators that don't supply metadata for `object`. * Address feedback
1 parent 693663b commit 3a01050

23 files changed

+317
-128
lines changed

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverter.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,29 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, object? va
111111
runtimeConverter.WriteAsPropertyNameCoreAsObject(writer, value, options, isWritingExtensionDataProperty);
112112
}
113113
}
114+
115+
/// <summary>
116+
/// A placeholder ObjectConverter used for driving object root value
117+
/// serialization only and does not root JsonNode/JsonDocument.
118+
/// </summary>
119+
internal sealed class ObjectConverterSlim : JsonConverter<object?>
120+
{
121+
private protected override ConverterStrategy GetDefaultConverterStrategy() => ConverterStrategy.Object;
122+
123+
public ObjectConverterSlim()
124+
{
125+
CanBePolymorphic = true;
126+
}
127+
128+
public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
129+
{
130+
Debug.Fail("Converter should only be used to drive root-level object serialization.");
131+
return null;
132+
}
133+
134+
public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options)
135+
{
136+
Debug.Fail("Converter should only be used to drive root-level object serialization.");
137+
}
138+
}
114139
}

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

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -80,21 +80,18 @@ 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);
83+
Type runtimeType = value.GetType();
8684

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;
85+
if (CanBePolymorphic && runtimeType != TypeToConvert)
86+
{
87+
Debug.Assert(TypeToConvert == typeof(object));
88+
jsonTypeInfo = state.Current.InitializePolymorphicReEntry(runtimeType, options);
89+
polymorphicConverter = jsonTypeInfo.Converter;
9190
}
9291

93-
Type runtimeType = value.GetType();
94-
9592
if (jsonTypeInfo.PolymorphicTypeResolver is PolymorphicTypeResolver resolver)
9693
{
97-
Debug.Assert(CanHaveMetadata);
94+
Debug.Assert(jsonTypeInfo.Converter.CanHaveMetadata);
9895

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

110107
state.PolymorphicTypeDiscriminator = typeDiscriminator;
108+
state.PolymorphicTypeResolver = resolver;
111109
}
112110
}
113-
else
114-
{
115-
state.Current.PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryNotFound;
116-
}
117111
}
118-
else
119-
{
120-
Debug.Assert(CanBePolymorphic);
121112

122-
if (runtimeType != TypeToConvert)
123-
{
124-
polymorphicConverter = state.Current.InitializePolymorphicReEntry(runtimeType, options);
125-
}
126-
else
127-
{
128-
state.Current.PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryNotFound;
129-
}
113+
if (polymorphicConverter is null)
114+
{
115+
state.Current.PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryNotFound;
130116
}
117+
131118
break;
132119

133120
case PolymorphicSerializationState.PolymorphicReEntrySuspended:

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali
248248
{
249249
// Special case object converters since they don't
250250
// require the expensive ReadStack.Push()/Pop() operations.
251-
Debug.Assert(this is ObjectConverter);
251+
Debug.Assert(this is ObjectConverter or ObjectConverterSlim);
252252
success = OnTryRead(ref reader, typeToConvert, options, ref state, out value);
253253
Debug.Assert(success);
254254
return true;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public static partial class JsonSerializer
2929

3030
[RequiresUnreferencedCode(SerializationUnreferencedCodeMessage)]
3131
[RequiresDynamicCode(SerializationRequiresDynamicCodeMessage)]
32-
private static JsonTypeInfo GetTypeInfo(JsonSerializerOptions? options, Type inputType, bool fallBackToNearestAncestorType = false)
32+
private static JsonTypeInfo GetTypeInfo(JsonSerializerOptions? options, Type inputType)
3333
{
3434
Debug.Assert(inputType != null);
3535

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

5151
[RequiresUnreferencedCode(SerializationUnreferencedCodeMessage)]

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.ByteArray.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public static byte[] SerializeToUtf8Bytes(
5555
JsonSerializerOptions? options = null)
5656
{
5757
ValidateInputType(value, inputType);
58-
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
58+
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType);
5959
return WriteBytesAsObject(value, jsonTypeInfo);
6060
}
6161

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public static JsonDocument SerializeToDocument<TValue>(TValue value, JsonSeriali
5151
public static JsonDocument SerializeToDocument(object? value, Type inputType, JsonSerializerOptions? options = null)
5252
{
5353
ValidateInputType(value, inputType);
54-
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
54+
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType);
5555
return WriteDocumentAsObject(value, jsonTypeInfo);
5656
}
5757

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public static JsonElement SerializeToElement<TValue>(TValue value, JsonSerialize
5151
public static JsonElement SerializeToElement(object? value, Type inputType, JsonSerializerOptions? options = null)
5252
{
5353
ValidateInputType(value, inputType);
54-
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
54+
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType);
5555
return WriteElementAsObject(value, jsonTypeInfo);
5656
}
5757

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ internal static MetadataPropertyName WriteMetadataForObject(
3434

3535
if (state.PolymorphicTypeDiscriminator is object discriminator)
3636
{
37-
Debug.Assert(state.Parent.JsonPropertyInfo!.JsonTypeInfo.PolymorphicTypeResolver != null);
37+
Debug.Assert(state.PolymorphicTypeResolver != null);
3838

3939
JsonEncodedText propertyName =
40-
state.Parent.JsonPropertyInfo.JsonTypeInfo.PolymorphicTypeResolver.CustomTypeDiscriminatorPropertyNameJsonEncoded is JsonEncodedText customPropertyName
40+
state.PolymorphicTypeResolver.CustomTypeDiscriminatorPropertyNameJsonEncoded is JsonEncodedText customPropertyName
4141
? customPropertyName
4242
: s_metadataType;
4343

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

91-
state.PolymorphicTypeDiscriminator = null; // clear out any polymorphism state.
91+
// clear out any polymorphism state.
92+
state.PolymorphicTypeDiscriminator = null;
93+
state.PolymorphicTypeResolver = null;
9294
}
9395
else
9496
{

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Node.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public static partial class JsonSerializer
5252
public static JsonNode? SerializeToNode(object? value, Type inputType, JsonSerializerOptions? options = null)
5353
{
5454
ValidateInputType(value, inputType);
55-
JsonTypeInfo typeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
55+
JsonTypeInfo typeInfo = GetTypeInfo(options, inputType);
5656
return WriteNodeAsObject(value, typeInfo);
5757
}
5858

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ public static Task SerializeAsync(
117117
}
118118

119119
ValidateInputType(value, inputType);
120-
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
120+
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType);
121121
return jsonTypeInfo.SerializeAsObjectAsync(utf8Json, value, cancellationToken);
122122
}
123123

@@ -152,7 +152,7 @@ public static void Serialize(
152152
}
153153

154154
ValidateInputType(value, inputType);
155-
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
155+
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType);
156156
jsonTypeInfo.SerializeAsObject(utf8Json, value);
157157
}
158158

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.String.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public static string Serialize(
6262
JsonSerializerOptions? options = null)
6363
{
6464
ValidateInputType(value, inputType);
65-
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
65+
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType);
6666
return WriteStringAsObject(value, jsonTypeInfo);
6767
}
6868

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Utf8JsonWriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public static void Serialize(
7070
}
7171

7272
ValidateInputType(value, inputType);
73-
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
73+
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType);
7474
jsonTypeInfo.SerializeAsObject(writer, value);
7575
}
7676

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Runtime.CompilerServices;
99
using System.Runtime.ExceptionServices;
1010
using System.Text.Json.Serialization;
11+
using System.Text.Json.Serialization.Converters;
1112
using System.Text.Json.Serialization.Metadata;
1213

1314
namespace System.Text.Json
@@ -161,7 +162,14 @@ internal bool TryGetPolymorphicTypeInfoForRootType(object rootValue, [NotNullWhe
161162
Type runtimeType = rootValue.GetType();
162163
if (runtimeType != JsonTypeInfo.ObjectType)
163164
{
165+
// To determine the contract for an object value:
166+
// 1. Find the JsonTypeInfo for the runtime type with fallback to the nearest ancestor, if not available.
167+
// 2. If the resolved type is deriving from a polymorphic type, use the contract of the polymorphic type instead.
164168
polymorphicTypeInfo = GetTypeInfoForRootType(runtimeType, fallBackToNearestAncestorType: true);
169+
if (polymorphicTypeInfo.AncestorPolymorphicType is { } ancestorPolymorphicType)
170+
{
171+
polymorphicTypeInfo = ancestorPolymorphicType;
172+
}
165173
return true;
166174
}
167175

@@ -175,7 +183,22 @@ internal JsonTypeInfo ObjectTypeInfo
175183
get
176184
{
177185
Debug.Assert(IsReadOnly);
178-
return _objectTypeInfo ??= GetTypeInfoInternal(JsonTypeInfo.ObjectType);
186+
return _objectTypeInfo ??= GetObjectTypeInfo(this);
187+
188+
static JsonTypeInfo GetObjectTypeInfo(JsonSerializerOptions options)
189+
{
190+
JsonTypeInfo? typeInfo = options.GetTypeInfoInternal(JsonTypeInfo.ObjectType, ensureNotNull: null);
191+
if (typeInfo is null)
192+
{
193+
// If the user-supplied resolver does not provide a JsonTypeInfo<object>,
194+
// use a placeholder value to drive root-level boxed value serialization.
195+
var converter = new ObjectConverterSlim();
196+
typeInfo = new JsonTypeInfo<object>(converter, options);
197+
typeInfo.EnsureConfigured();
198+
}
199+
200+
return typeInfo;
201+
}
179202
}
180203
}
181204

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,30 @@ private void Configure()
629629
}
630630
}
631631

632+
/// <summary>
633+
/// Gets any ancestor polymorphic types that declare
634+
/// a type discriminator for the current type. Consulted
635+
/// when serializing polymorphic values as objects.
636+
/// </summary>
637+
internal JsonTypeInfo? AncestorPolymorphicType
638+
{
639+
get
640+
{
641+
Debug.Assert(IsConfigured);
642+
643+
if (!_isAncestorPolymorphicTypeResolved)
644+
{
645+
_ancestorPolymorhicType = PolymorphicTypeResolver.FindNearestPolymorphicBaseType(this);
646+
_isAncestorPolymorphicTypeResolved = true;
647+
}
648+
649+
return _ancestorPolymorhicType;
650+
}
651+
}
652+
653+
private JsonTypeInfo? _ancestorPolymorhicType;
654+
private volatile bool _isAncestorPolymorphicTypeResolved;
655+
632656
/// <summary>
633657
/// Determines if the transitive closure of all JsonTypeInfo metadata referenced
634658
/// by the current type (property types, key types, element types, ...) are
@@ -877,9 +901,9 @@ public JsonPropertyInfo CreateJsonPropertyInfo(Type propertyType, string name)
877901
internal JsonParameterInfoValues[]? ParameterInfoValues { get; set; }
878902

879903
// Untyped, root-level serialization methods
880-
internal abstract void SerializeAsObject(Utf8JsonWriter writer, object? rootValue, bool isInvokedByPolymorphicConverter = false);
881-
internal abstract Task SerializeAsObjectAsync(Stream utf8Json, object? rootValue, CancellationToken cancellationToken, bool isInvokedByPolymorphicConverter = false);
882-
internal abstract void SerializeAsObject(Stream utf8Json, object? rootValue, bool isInvokedByPolymorphicConverter = false);
904+
internal abstract void SerializeAsObject(Utf8JsonWriter writer, object? rootValue);
905+
internal abstract Task SerializeAsObjectAsync(Stream utf8Json, object? rootValue, CancellationToken cancellationToken);
906+
internal abstract void SerializeAsObject(Stream utf8Json, object? rootValue);
883907

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

0 commit comments

Comments
 (0)