Skip to content

Commit dcd0506

Browse files
Implement support for unspeakable types. (#83631)
* Implement support for unspeakable types. * Remove unneeded declarations. * Record ambiguity information in exception message. * Fix error message testing in wasm. * Address feedback. * Add test case for interface POCOs * Update src/libraries/System.Text.Json/src/Resources/Strings.resx Co-authored-by: Dan Moseley <[email protected]> --------- Co-authored-by: Dan Moseley <[email protected]>
1 parent 1f6fc66 commit dcd0506

14 files changed

+361
-31
lines changed

src/libraries/System.Text.Json/src/Resources/Strings.resx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,10 @@
594594
<value>The generic type of the converter for property '{0}.{1}' must match with the specified converter type '{2}'. The converter must not be 'null'.</value>
595595
</data>
596596
<data name="NoMetadataForType" xml:space="preserve">
597-
<value>Metadata for type '{0}' was not provided by TypeInfoResolver of type '{1}'. If using source generation, ensure that all root types passed to the serializer have been indicated with 'JsonSerializableAttribute', along with any types that might be serialized polymorphically.</value>
597+
<value>JsonTypeInfo metadata for type '{0}' was not provided by TypeInfoResolver of type '{1}'. If using source generation, ensure that all root types passed to the serializer have been annotated with 'JsonSerializableAttribute', along with any types that might be serialized polymorphically.</value>
598+
</data>
599+
<data name="AmbiguousMetadataForType" xml:space="preserve">
600+
<value>Ambiguous matches when resolving JsonTypeInfo metadata for type '{0}': '{1}', '{2}'. Consider either explicitly providing metadata for the type or removing one of its interface implementations.</value>
598601
</data>
599602
<data name="CollectionIsReadOnly" xml:space="preserve">
600603
<value>Collection is read-only.</value>

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
@@ -15,7 +15,7 @@ public static partial class JsonSerializer
1515

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

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

3737
[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);
58+
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
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);
54+
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
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);
54+
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
5555
return WriteElementAsObject(value, jsonTypeInfo);
5656
}
5757

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);
55+
JsonTypeInfo typeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
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);
120+
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
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);
155+
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
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);
65+
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
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);
73+
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType, fallBackToNearestAncestorType: true);
7474
jsonTypeInfo.SerializeAsObject(writer, value);
7575
}
7676

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

Lines changed: 167 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
using System.Diagnostics;
77
using System.Diagnostics.CodeAnalysis;
88
using System.Runtime.CompilerServices;
9+
using System.Runtime.ExceptionServices;
910
using System.Text.Json.Serialization;
1011
using System.Text.Json.Serialization.Metadata;
11-
using System.Threading;
1212

1313
namespace System.Text.Json
1414
{
@@ -62,13 +62,19 @@ public JsonTypeInfo GetTypeInfo(Type type)
6262
/// <summary>
6363
/// Same as GetTypeInfo but without validation and additional knobs.
6464
/// </summary>
65-
internal JsonTypeInfo GetTypeInfoInternal(Type type, bool ensureConfigured = true, bool resolveIfMutable = false)
65+
internal JsonTypeInfo GetTypeInfoInternal(
66+
Type type,
67+
bool ensureConfigured = true,
68+
bool resolveIfMutable = false,
69+
bool fallBackToNearestAncestorType = false)
6670
{
71+
Debug.Assert(!fallBackToNearestAncestorType || IsReadOnly, "ancestor resolution should only be invoked in read-only options.");
72+
6773
JsonTypeInfo? typeInfo = null;
6874

6975
if (IsReadOnly)
7076
{
71-
typeInfo = CacheContext.GetOrAddTypeInfo(type);
77+
typeInfo = CacheContext.GetOrAddTypeInfo(type, fallBackToNearestAncestorType);
7278
if (ensureConfigured)
7379
{
7480
typeInfo?.EnsureConfigured();
@@ -95,21 +101,20 @@ internal bool TryGetTypeInfoCached(Type type, [NotNullWhen(true)] out JsonTypeIn
95101
return false;
96102
}
97103

98-
return _cachingContext.TryGetJsonTypeInfo(type, out typeInfo);
104+
return _cachingContext.TryGetTypeInfo(type, out typeInfo);
99105
}
100106

101107
/// <summary>
102108
/// Return the TypeInfo for root API calls.
103109
/// This has an LRU cache that is intended only for public API calls that specify the root type.
104110
/// </summary>
105-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
106-
internal JsonTypeInfo GetTypeInfoForRootType(Type type)
111+
internal JsonTypeInfo GetTypeInfoForRootType(Type type, bool fallBackToNearestAncestorType = false)
107112
{
108113
JsonTypeInfo? jsonTypeInfo = _lastTypeInfo;
109114

110115
if (jsonTypeInfo?.Type != type)
111116
{
112-
_lastTypeInfo = jsonTypeInfo = GetTypeInfoInternal(type);
117+
_lastTypeInfo = jsonTypeInfo = GetTypeInfoInternal(type, fallBackToNearestAncestorType: fallBackToNearestAncestorType);
113118
}
114119

115120
return jsonTypeInfo;
@@ -122,7 +127,7 @@ internal bool TryGetPolymorphicTypeInfoForRootType(object rootValue, [NotNullWhe
122127
Type runtimeType = rootValue.GetType();
123128
if (runtimeType != JsonTypeInfo.ObjectType)
124129
{
125-
polymorphicTypeInfo = GetTypeInfoForRootType(runtimeType);
130+
polymorphicTypeInfo = GetTypeInfoForRootType(runtimeType, fallBackToNearestAncestorType: true);
126131
return true;
127132
}
128133

@@ -157,39 +162,182 @@ internal void ClearCaches()
157162
/// </summary>
158163
internal sealed class CachingContext
159164
{
160-
private readonly ConcurrentDictionary<Type, JsonTypeInfo?> _jsonTypeInfoCache = new();
165+
private readonly ConcurrentDictionary<Type, CacheEntry> _cache = new();
161166
#if !NETCOREAPP
162-
private readonly Func<Type, JsonTypeInfo?> _jsonTypeInfoFactory;
167+
private readonly Func<Type, CacheEntry> _cacheEntryFactory;
163168
#endif
164169

165170
public CachingContext(JsonSerializerOptions options, int hashCode)
166171
{
167172
Options = options;
168173
HashCode = hashCode;
169174
#if !NETCOREAPP
170-
_jsonTypeInfoFactory = Options.GetTypeInfoNoCaching;
175+
_cacheEntryFactory = type => CreateCacheEntry(type, this);
171176
#endif
172177
}
173178

174179
public JsonSerializerOptions Options { get; }
175180
public int HashCode { get; }
176181
// Property only accessed by reflection in testing -- do not remove.
177182
// If changing please ensure that src/ILLink.Descriptors.LibraryBuild.xml is up-to-date.
178-
public int Count => _jsonTypeInfoCache.Count;
183+
public int Count => _cache.Count;
184+
185+
public JsonTypeInfo? GetOrAddTypeInfo(Type type, bool fallBackToNearestAncestorType = false)
186+
{
187+
CacheEntry entry = GetOrAddCacheEntry(type);
188+
return fallBackToNearestAncestorType && !entry.HasResult
189+
? FallBackToNearestAncestor(type, entry)
190+
: entry.GetResult();
191+
}
192+
193+
public bool TryGetTypeInfo(Type type, [NotNullWhen(true)] out JsonTypeInfo? typeInfo)
194+
{
195+
_cache.TryGetValue(type, out CacheEntry? entry);
196+
typeInfo = entry?.TypeInfo;
197+
return typeInfo is not null;
198+
}
179199

180-
public JsonTypeInfo? GetOrAddTypeInfo(Type type) =>
200+
public void Clear()
201+
{
202+
_cache.Clear();
203+
}
204+
205+
private CacheEntry GetOrAddCacheEntry(Type type)
206+
{
181207
#if NETCOREAPP
182-
_jsonTypeInfoCache.GetOrAdd(type, static (type, options) => options.GetTypeInfoNoCaching(type), Options);
208+
return _cache.GetOrAdd(type, CreateCacheEntry, this);
183209
#else
184-
_jsonTypeInfoCache.GetOrAdd(type, _jsonTypeInfoFactory);
210+
return _cache.GetOrAdd(type, _cacheEntryFactory);
185211
#endif
212+
}
186213

187-
public bool TryGetJsonTypeInfo(Type type, [NotNullWhen(true)] out JsonTypeInfo? typeInfo)
188-
=> _jsonTypeInfoCache.TryGetValue(type, out typeInfo);
214+
private static CacheEntry CreateCacheEntry(Type type, CachingContext context)
215+
{
216+
try
217+
{
218+
JsonTypeInfo? typeInfo = context.Options.GetTypeInfoNoCaching(type);
219+
return new CacheEntry(typeInfo);
220+
}
221+
catch (Exception ex)
222+
{
223+
ExceptionDispatchInfo edi = ExceptionDispatchInfo.Capture(ex);
224+
return new CacheEntry(edi);
225+
}
226+
}
189227

190-
public void Clear()
228+
private JsonTypeInfo? FallBackToNearestAncestor(Type type, CacheEntry entry)
229+
{
230+
Debug.Assert(!entry.HasResult);
231+
232+
CacheEntry? nearestAncestor = entry.IsNearestAncestorResolved
233+
? entry.NearestAncestor
234+
: DetermineNearestAncestor(type, entry);
235+
236+
return nearestAncestor?.GetResult();
237+
}
238+
239+
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern",
240+
Justification = "We only need to examine the interface types that are supported by the underlying resolver.")]
241+
private CacheEntry? DetermineNearestAncestor(Type type, CacheEntry entry)
242+
{
243+
// In cases where the underlying TypeInfoResolver returns `null` for a given type,
244+
// this method traverses the hierarchy above the given type to determine potential
245+
// ancestors for which the resolver does provide metadata. This can be useful in
246+
// cases where we're using a source generator and are trying to serialize private
247+
// implementations of an interface that is supported by the source generator.
248+
// NB this algorithm runs lazily and unsynchronized *after* the CacheEntry has been looked up
249+
// from the global cache, so care should be taken to avoid potential race conditions.
250+
//
251+
// IMPORTANT: nearest-ancestor resolution should be reserved for weakly-typed serialization.
252+
// Attempting to use it in strongly typed operations or deserialization will invariably
253+
// result in an invalid cast exception, so use with caution.
254+
255+
Debug.Assert(!entry.HasResult);
256+
CacheEntry? candidate = null;
257+
Type? candidateType = null;
258+
259+
for (Type? current = type.BaseType; current != null; current = current.BaseType)
260+
{
261+
if (current == JsonTypeInfo.ObjectType)
262+
{
263+
// Avoid falling back to the contract for object since it's polymorphic
264+
// and it would try to send us back to the runtime type that isn't supported.
265+
break;
266+
}
267+
268+
candidate = GetOrAddCacheEntry(current);
269+
if (candidate.HasResult)
270+
{
271+
// We found a type in the class hierarchy that has a contract -- stop looking further up.
272+
candidateType = current;
273+
break;
274+
}
275+
}
276+
277+
foreach (Type interfaceType in type.GetInterfaces())
278+
{
279+
CacheEntry interfaceEntry = GetOrAddCacheEntry(interfaceType);
280+
if (interfaceEntry.HasResult)
281+
{
282+
if (candidateType != null)
283+
{
284+
if (interfaceType.IsAssignableFrom(candidateType))
285+
{
286+
// The previous candidate is more derived than the
287+
// current interface -- keep our previous choice.
288+
continue;
289+
}
290+
else if (candidateType.IsAssignableFrom(interfaceType))
291+
{
292+
// The current interface is more derived than the
293+
// previous candidate -- replace the candidate value.
294+
}
295+
else
296+
{
297+
// We have found two possible ancestors that are not in subtype relationship.
298+
// This indicates we have encountered a diamond ambiguity -- abort search and record an exception.
299+
NotSupportedException nse = ThrowHelper.GetNotSupportedException_AmbiguousMetadataForType(type, candidateType, interfaceType);
300+
candidate = new CacheEntry(ExceptionDispatchInfo.Capture(nse));
301+
break;
302+
}
303+
}
304+
305+
candidate = interfaceEntry;
306+
candidateType = interfaceType;
307+
}
308+
}
309+
310+
entry.NearestAncestor = candidate;
311+
entry.IsNearestAncestorResolved = true;
312+
return candidate;
313+
}
314+
315+
private sealed class CacheEntry
191316
{
192-
_jsonTypeInfoCache.Clear();
317+
public readonly bool HasResult;
318+
public readonly JsonTypeInfo? TypeInfo;
319+
public readonly ExceptionDispatchInfo? ExceptionDispatchInfo;
320+
321+
public volatile bool IsNearestAncestorResolved;
322+
public CacheEntry? NearestAncestor;
323+
324+
public CacheEntry(JsonTypeInfo? typeInfo)
325+
{
326+
TypeInfo = typeInfo;
327+
HasResult = typeInfo is not null;
328+
}
329+
330+
public CacheEntry(ExceptionDispatchInfo exception)
331+
{
332+
ExceptionDispatchInfo = exception;
333+
HasResult = true;
334+
}
335+
336+
public JsonTypeInfo? GetResult()
337+
{
338+
ExceptionDispatchInfo?.Throw();
339+
return TypeInfo;
340+
}
193341
}
194342
}
195343

0 commit comments

Comments
 (0)