Skip to content

Commit

Permalink
Add a JsonSerializerOptions.TryGetTypeInfo method. (#84411)
Browse files Browse the repository at this point in the history
* Mark the return type of the JsonSerializer.GetTypeInfo method as nullable.

* Use new TryGetTypeInfo method instead.
  • Loading branch information
eiriktsarpalis committed Apr 12, 2023
1 parent c31d264 commit 0b08111
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 27 deletions.
29 changes: 6 additions & 23 deletions src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1322,33 +1322,16 @@ private string GetGetTypeInfoImplementation()
{
StringBuilder sb = new();

// JsonSerializerContext.GetTypeInfo override -- returns cached metadata via JsonSerializerOptions
sb.Append(
@$"/// <inheritdoc/>
public override {JsonTypeInfoTypeRef}? GetTypeInfo({TypeTypeRef} type)
{{");
// This method body grows linearly over the number of generated types.
// In line with https://github.com/dotnet/runtime/issues/77897 we should
// eventually replace this method with a direct call to Options.GetTypeInfo().
// We can't do this currently because Options.GetTypeInfo throws whereas
// this GetTypeInfo returns null for unsupported types, so we need new API to support it.
foreach (TypeGenerationSpec metadata in _currentContext.TypesWithMetadataGenerated)
{
if (metadata.ClassType != ClassType.TypeUnsupportedBySourceGen)
{
sb.Append($@"
if (type == typeof({metadata.TypeRef}))
{{
return this.{metadata.TypeInfoPropertyName};
}}
{{
{OptionsInstanceVariableName}.TryGetTypeInfo(type, out {JsonTypeInfoTypeRef}? typeInfo);
return typeInfo;
}}
");
}
}

sb.AppendLine(@"
return null;
}");

// Explicit IJsonTypeInfoResolver implementation
// Explicit IJsonTypeInfoResolver implementation -- the source of truth for metadata resolution
sb.AppendLine();
sb.Append(@$"{JsonTypeInfoTypeRef}? {JsonTypeInfoResolverTypeRef}.GetTypeInfo({TypeTypeRef} type, {JsonSerializerOptionsTypeRef} {OptionsLocalVariableName})
{{");
Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Text.Json/ref/System.Text.Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { }
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("Getting a converter for a type may require reflection which depends on unreferenced code.")]
public System.Text.Json.Serialization.JsonConverter GetConverter(System.Type typeToConvert) { throw null; }
public System.Text.Json.Serialization.Metadata.JsonTypeInfo GetTypeInfo(System.Type type) { throw null; }
public bool TryGetTypeInfo(System.Type type, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Text.Json.Serialization.Metadata.JsonTypeInfo typeInfo) { throw null; }
public void MakeReadOnly() { }
}
public enum JsonTokenType : byte
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,50 @@ public JsonTypeInfo GetTypeInfo(Type type)
return GetTypeInfoInternal(type, resolveIfMutable: true);
}

/// <summary>
/// Tries to get the <see cref="JsonTypeInfo"/> contract metadata resolved by the current <see cref="JsonSerializerOptions"/> instance.
/// </summary>
/// <param name="type">The type to resolve contract metadata for.</param>
/// <param name="typeInfo">The resolved contract metadata, or <see langword="null" /> if not contract could be resolved.</param>
/// <returns><see langword="true"/> if a contract for <paramref name="type"/> was found, or <see langword="false"/> otherwise.</returns>
/// <exception cref="ArgumentNullException"><paramref name="type"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="type"/> is not valid for serialization.</exception>
/// <remarks>
/// Returned metadata can be downcast to <see cref="JsonTypeInfo{T}"/> and used with the relevant <see cref="JsonSerializer"/> overloads.
///
/// If the <see cref="JsonSerializerOptions"/> instance is locked for modification, the method will return a cached instance for the metadata.
/// </remarks>
public bool TryGetTypeInfo(Type type, [NotNullWhen(true)] out JsonTypeInfo? typeInfo)
{
if (type is null)
{
ThrowHelper.ThrowArgumentNullException(nameof(type));
}

if (JsonTypeInfo.IsInvalidForSerialization(type))
{
ThrowHelper.ThrowArgumentException_CannotSerializeInvalidType(nameof(type), type, null, null);
}

typeInfo = GetTypeInfoInternal(type, ensureNotNull: null, resolveIfMutable: true);
return typeInfo is not null;
}

/// <summary>
/// Same as GetTypeInfo but without validation and additional knobs.
/// </summary>
internal JsonTypeInfo GetTypeInfoInternal(
[return: NotNullIfNotNull(nameof(ensureNotNull))]
internal JsonTypeInfo? GetTypeInfoInternal(
Type type,
bool ensureConfigured = true,
// We can't assert non-nullability on the basis of boolean parameters,
// so use a nullable representation instead to piggy-back on the NotNullIfNotNull attribute.
bool? ensureNotNull = true,
bool resolveIfMutable = false,
bool fallBackToNearestAncestorType = false)
{
Debug.Assert(!fallBackToNearestAncestorType || IsReadOnly, "ancestor resolution should only be invoked in read-only options.");
Debug.Assert(ensureNotNull is null or true, "Explicitly passing false will result in invalid result annotation.");

JsonTypeInfo? typeInfo = null;

Expand All @@ -85,7 +119,7 @@ internal JsonTypeInfo GetTypeInfoInternal(
typeInfo = GetTypeInfoNoCaching(type);
}

if (typeInfo == null)
if (typeInfo is null && ensureNotNull == true)
{
ThrowHelper.ThrowNotSupportedException_NoMetadataForType(type, TypeInfoResolver);
}
Expand Down Expand Up @@ -241,7 +275,7 @@ private static CacheEntry CreateCacheEntry(Type type, CachingContext context)
private CacheEntry? DetermineNearestAncestor(Type type, CacheEntry entry)
{
// In cases where the underlying TypeInfoResolver returns `null` for a given type,
// this method traverses the hierarchy above the given type to determine potential
// this method traverses the hierarchy above the type to determine potential
// ancestors for which the resolver does provide metadata. This can be useful in
// cases where we're using a source generator and are trying to serialize private
// implementations of an interface that is supported by the source generator.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,21 @@ internal void ConfigureForJsonSerializer()
case JsonSerializerContext ctx when AppContextSwitchHelper.IsSourceGenReflectionFallbackEnabled:
// .NET 6 compatibility mode: enable fallback to reflection metadata for JsonSerializerContext
_effectiveJsonTypeInfoResolver = JsonTypeInfoResolver.Combine(ctx, defaultResolver);

if (_cachingContext is { } cachingContext)
{
// A cache has already been created by the source generator.
// Repeat the same configuration routine for that options instance, if different.
// Invalidate any cache entries that have already been stored.
if (cachingContext.Options != this)
{
cachingContext.Options.ConfigureForJsonSerializer();
}
else
{
cachingContext.Clear();
}
}
break;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,8 @@ public static void Options_DisablingIsReflectionEnabledByDefaultSwitch_DefaultOp
Assert.Throws<NotSupportedException>(() => options.GetTypeInfo(typeof(string)));
Assert.Throws<NotSupportedException>(() => options.GetConverter(typeof(string)));
Assert.False(options.TryGetTypeInfo(typeof(string), out JsonTypeInfo? typeInfo));
Assert.Null(typeInfo);
Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize("string"));
Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize("string", options));
Expand Down Expand Up @@ -552,6 +554,8 @@ public static void Options_DisablingIsReflectionEnabledByDefaultSwitch_NewOption
Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize("string", options));
Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<string>("\"string\"", options));
Assert.False(options.TryGetTypeInfo(typeof(string), out JsonTypeInfo? typeInfo));
Assert.Null(typeInfo);
Assert.False(options.IsReadOnly); // failed operations should not lock the instance
Expand Down Expand Up @@ -661,6 +665,10 @@ public static void Options_JsonSerializerContext_Net6CompatibilitySwitch_FallsBa
JsonConverter converter = JsonContext.Default.Options.GetConverter(typeof(MyClass));
Assert.IsAssignableFrom<JsonConverter<MyClass>>(converter);
// Serialization using JsonSerializerContext now uses the reflection fallback.
json = JsonSerializer.Serialize(unsupportedValue, unsupportedValue.GetType(), JsonContext.Default);
JsonTestHelper.AssertJsonEqual("""{"Value":"value", "Thing":null}""", json);
}, options).Dispose();
}

Expand Down Expand Up @@ -1373,9 +1381,12 @@ public static void GetTypeInfo_MutableOptionsInstance(Type type)

// An unset resolver results in NotSupportedException.
Assert.Throws<NotSupportedException>(() => options.GetTypeInfo(type));
// And returns false in the Try-method
Assert.False(options.TryGetTypeInfo(type, out JsonTypeInfo? typeInfo));
Assert.Null(typeInfo);

options.TypeInfoResolver = new DefaultJsonTypeInfoResolver();
JsonTypeInfo typeInfo = options.GetTypeInfo(type);
typeInfo = options.GetTypeInfo(type);
Assert.Equal(type, typeInfo.Type);
Assert.False(typeInfo.IsReadOnly);

Expand All @@ -1385,6 +1396,12 @@ public static void GetTypeInfo_MutableOptionsInstance(Type type)

Assert.NotSame(typeInfo, typeInfo2);

Assert.True(options.TryGetTypeInfo(type, out JsonTypeInfo? typeInfo3));
Assert.Equal(type, typeInfo3.Type);
Assert.False(typeInfo3.IsReadOnly);

Assert.NotSame(typeInfo, typeInfo3);

options.WriteIndented = true; // can mutate without issue
}

Expand All @@ -1404,6 +1421,9 @@ public static void GetTypeInfo_ImmutableOptionsInstance(Type type)

JsonTypeInfo typeInfo2 = options.GetTypeInfo(type);
Assert.Same(typeInfo, typeInfo2);

Assert.True(options.TryGetTypeInfo(type, out JsonTypeInfo? typeInfo3));
Assert.Same(typeInfo, typeInfo3);
}

[Fact]
Expand Down Expand Up @@ -1451,6 +1471,7 @@ public static void GetTypeInfo_NullInput_ThrowsArgumentNullException()
{
var options = new JsonSerializerOptions();
Assert.Throws<ArgumentNullException>(() => options.GetTypeInfo(null));
Assert.Throws<ArgumentNullException>(() => options.TryGetTypeInfo(null, out JsonTypeInfo? _));
}

[Fact]
Expand Down Expand Up @@ -1503,6 +1524,7 @@ public static void GetTypeInfo_InvalidInput_ThrowsArgumentException(Type type)
{
var options = new JsonSerializerOptions();
Assert.Throws<ArgumentException>(() => options.GetTypeInfo(type));
Assert.Throws<ArgumentException>(() => options.TryGetTypeInfo(type, out JsonTypeInfo? _));
}

[Fact]
Expand All @@ -1512,6 +1534,9 @@ public static void GetTypeInfo_ResolverWithoutMetadata_ThrowsNotSupportedExcepti
options.AddContext<JsonContext>();

Assert.Throws<NotSupportedException>(() => options.GetTypeInfo(typeof(BasicCompany)));

Assert.False(options.TryGetTypeInfo(typeof(BasicCompany), out JsonTypeInfo? typeInfo));
Assert.Null(typeInfo);
}

[Theory]
Expand Down

0 comments on commit 0b08111

Please sign in to comment.