Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions src/libraries/System.Text.Json/ref/System.Text.Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -457,11 +457,13 @@ 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 System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> GetTypeInfo<T>() { throw null; }
public void MakeReadOnly() { }
[System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("Populating unconfigured TypeInfoResolver properties with the reflection resolver requires runtime code generation.")]
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("Populating unconfigured TypeInfoResolver properties with the reflection resolver requires unreferenced code.")]
public void MakeReadOnly(bool populateMissingResolver) { }
public bool TryGetTypeInfo(System.Type type, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Text.Json.Serialization.Metadata.JsonTypeInfo? typeInfo) { throw null; }
public bool TryGetTypeInfo<T>([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Text.Json.Serialization.Metadata.JsonTypeInfo<T>? typeInfo) { throw null; }
}
public enum JsonTokenType : byte
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,37 @@ public bool TryGetTypeInfo(Type type, [NotNullWhen(true)] out JsonTypeInfo? type
return typeInfo is not null;
}

/// <summary>
/// Gets the <see cref="JsonTypeInfo{T}"/> contract metadata resolved by the current <see cref="JsonSerializerOptions"/> instance.
/// </summary>
/// <typeparam name="T">The type to resolve contract metadata for.</typeparam>
/// <returns>The contract metadata resolved for <typeparamref name="T"/>.</returns>
/// <exception cref="ArgumentException"><typeparamref name="T"/> is not valid for serialization.</exception>
/// <remarks>
/// If the <see cref="JsonSerializerOptions"/> instance is locked for modification, the method will return a cached instance for the metadata.
/// </remarks>
public JsonTypeInfo<T> GetTypeInfo<T>()
{
return (JsonTypeInfo<T>)GetTypeInfo(typeof(T));
}
Comment thread
stephentoub marked this conversation as resolved.

/// <summary>
/// Tries to get the <see cref="JsonTypeInfo{T}"/> contract metadata resolved by the current <see cref="JsonSerializerOptions"/> instance.
/// </summary>
/// <typeparam name="T">The type to resolve contract metadata for.</typeparam>
/// <param name="typeInfo">The resolved contract metadata, or <see langword="null" /> if no contract could be resolved.</param>
/// <returns><see langword="true"/> if a contract for <typeparamref name="T"/> was found, or <see langword="false"/> otherwise.</returns>
/// <exception cref="ArgumentException"><typeparamref name="T"/> is not valid for serialization.</exception>
/// <remarks>
/// If the <see cref="JsonSerializerOptions"/> instance is locked for modification, the method will return a cached instance for the metadata.
/// </remarks>
public bool TryGetTypeInfo<T>([NotNullWhen(true)] out JsonTypeInfo<T>? typeInfo)
{
bool success = TryGetTypeInfo(typeof(T), out JsonTypeInfo? nonGeneric);
typeInfo = (JsonTypeInfo<T>?)nonGeneric;
return success;
}

/// <summary>
/// Same as GetTypeInfo but without validation and additional knobs.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,12 @@ public virtual JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions? option
return mutable ? options.TypeInfoResolver.GetTypeInfo(type, options) : options.GetTypeInfo(type);
}

public JsonTypeInfo<T> GetTypeInfo<T>(JsonSerializerOptions? options = null,bool mutable = false)
=> (JsonTypeInfo<T>)GetTypeInfo(typeof(T), options, mutable);
public JsonTypeInfo<T> GetTypeInfo<T>(JsonSerializerOptions? options = null, bool mutable = false)
{
options ??= DefaultOptions;
options.MakeReadOnly(populateMissingResolver: true);
return mutable ? (JsonTypeInfo<T>)options.TypeInfoResolver.GetTypeInfo(typeof(T), options) : options.GetTypeInfo<T>();
}
Comment thread
stephentoub marked this conversation as resolved.
Outdated

public JsonSerializerOptions GetDefaultOptionsWithMetadataModifier(Action<JsonTypeInfo> modifier)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ public static void FastPathSerialization_ResolvingJsonTypeInfo()
{
JsonSerializerOptions options = FastPathSerializationContext.Default.Options;

JsonTypeInfo<JsonMessage> jsonMessageInfo = (JsonTypeInfo<JsonMessage>)options.GetTypeInfo(typeof(JsonMessage));
JsonTypeInfo<JsonMessage> jsonMessageInfo = options.GetTypeInfo<JsonMessage>();
Assert.NotNull(jsonMessageInfo.SerializeHandler);

var value = new JsonMessage { Message = "Hi" };
Expand Down Expand Up @@ -325,7 +325,7 @@ public static void FastPathSerialization_AppendedResolver_WorksAsExpected(IJsonT
TypeInfoResolver = JsonTypeInfoResolver.Combine(fastPathContext, appendedResolver, new DefaultJsonTypeInfoResolver())
};

JsonTypeInfo<PocoWithInteger> jsonMessageInfo = (JsonTypeInfo<PocoWithInteger>)options.GetTypeInfo(typeof(PocoWithInteger));
JsonTypeInfo<PocoWithInteger> jsonMessageInfo = options.GetTypeInfo<PocoWithInteger>();
Assert.NotNull(jsonMessageInfo.SerializeHandler);

var value = new PocoWithInteger { Value = 42 };
Expand All @@ -339,7 +339,7 @@ public static void FastPathSerialization_AppendedResolver_WorksAsExpected(IJsonT
Assert.Equal(expectedJson, json);
Assert.Equal(2, fastPathContext.FastPathInvocationCount);

JsonTypeInfo<ContainingClass> classInfo = (JsonTypeInfo<ContainingClass>)options.GetTypeInfo(typeof(ContainingClass));
JsonTypeInfo<ContainingClass> classInfo = options.GetTypeInfo<ContainingClass>();
Assert.Null(classInfo.SerializeHandler);

var largerValue = new ContainingClass { Message = value };
Expand Down Expand Up @@ -367,7 +367,7 @@ public static void FastPathSerialization_PrependedResolver_CompatibleResolvers_W
TypeInfoResolver = JsonTypeInfoResolver.Combine(prependedResolver, fastPathContext, new DefaultJsonTypeInfoResolver())
};

JsonTypeInfo<PocoWithInteger> jsonMessageInfo = (JsonTypeInfo<PocoWithInteger>)options.GetTypeInfo(typeof(PocoWithInteger));
JsonTypeInfo<PocoWithInteger> jsonMessageInfo = options.GetTypeInfo<PocoWithInteger>();
Assert.NotNull(jsonMessageInfo.SerializeHandler);

var value = new PocoWithInteger { Value = 42 };
Expand All @@ -381,7 +381,7 @@ public static void FastPathSerialization_PrependedResolver_CompatibleResolvers_W
Assert.Equal(expectedJson, json);
Assert.Equal(2, fastPathContext.FastPathInvocationCount);

JsonTypeInfo<ContainingClass> classInfo = (JsonTypeInfo<ContainingClass>)options.GetTypeInfo(typeof(ContainingClass));
JsonTypeInfo<ContainingClass> classInfo = options.GetTypeInfo<ContainingClass>();
Assert.Null(classInfo.SerializeHandler);

var largerValue = new ContainingClass { Message = value };
Expand Down Expand Up @@ -409,7 +409,7 @@ public static void FastPathSerialization_PrependedResolver_IncompatibleResolvers
TypeInfoResolver = JsonTypeInfoResolver.Combine(prependedResolver, fastPathContext, new DefaultJsonTypeInfoResolver())
};

JsonTypeInfo<PocoWithInteger> jsonMessageInfo = (JsonTypeInfo<PocoWithInteger>)options.GetTypeInfo(typeof(PocoWithInteger));
JsonTypeInfo<PocoWithInteger> jsonMessageInfo = options.GetTypeInfo<PocoWithInteger>();
Assert.NotNull(jsonMessageInfo.SerializeHandler);

var value = new PocoWithInteger { Value = 42 };
Expand All @@ -423,7 +423,7 @@ public static void FastPathSerialization_PrependedResolver_IncompatibleResolvers
Assert.Equal(expectedJson, json);
Assert.Equal(0, fastPathContext.FastPathInvocationCount);

JsonTypeInfo<ContainingClass> classInfo = (JsonTypeInfo<ContainingClass>)options.GetTypeInfo(typeof(ContainingClass));
JsonTypeInfo<ContainingClass> classInfo = options.GetTypeInfo<ContainingClass>();
Assert.Null(classInfo.SerializeHandler);

var largerValue = new ContainingClass { Message = value };
Expand Down Expand Up @@ -564,7 +564,7 @@ public static void ChainedContexts_Serialization<T>(T value, string expectedJson
{
var options = new JsonSerializerOptions { TypeInfoResolverChain = { NestedContext.Default, PersonJsonContext.Default } };

JsonTypeInfo<T> typeInfo = (JsonTypeInfo<T>)options.GetTypeInfo(typeof(T))!;
JsonTypeInfo<T> typeInfo = options.GetTypeInfo<T>();

string json = JsonSerializer.Serialize(value, typeInfo);
JsonTestHelper.AssertJsonEqual(expectedJson, json);
Expand Down Expand Up @@ -822,7 +822,7 @@ public TestResolver(Func<Type, JsonSerializerOptions, JsonTypeInfo?> getTypeInfo
public static void FastPathSerialization_EvaluatePropertyOnlyOnceWhenIgnoreNullOrDefaultIsSpecified()
{
JsonSerializerOptions options = FastPathSerializationContext.Default.Options;
JsonTypeInfo<AllocatingOnPropertyAccess> allocatingOnPropertyAccessInfo = (JsonTypeInfo<AllocatingOnPropertyAccess>)options.GetTypeInfo(typeof(AllocatingOnPropertyAccess));
JsonTypeInfo<AllocatingOnPropertyAccess> allocatingOnPropertyAccessInfo = options.GetTypeInfo<AllocatingOnPropertyAccess>();
Assert.NotNull(allocatingOnPropertyAccessInfo.SerializeHandler);

var value = new AllocatingOnPropertyAccess();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ public static void GetValueKind()
public static void GetValueKind_NumberHandling(JsonNumberHandling numberHandling, JsonValueKind expectedKind)
{
JsonSerializerOptions options = new(JsonSerializerOptions.Default) { NumberHandling = numberHandling };
JsonTypeInfo<int> typeInfo = (JsonTypeInfo<int>)options.GetTypeInfo(typeof(int));
JsonTypeInfo<int> typeInfo = options.GetTypeInfo<int>();
JsonValue value = JsonValue.Create(42, typeInfo);
Assert.Equal(expectedKind, value.GetValueKind());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ void TestValue<T>(T value, string expectedJson)
string json = JsonSerializer.Serialize(value, options);
Assert.Equal(expectedJson, json);

JsonTypeInfo<T> jsonTypeInfo = (JsonTypeInfo<T>)options.GetTypeInfo(typeof(T));
JsonTypeInfo<T> jsonTypeInfo = options.GetTypeInfo<T>();
json = JsonSerializer.Serialize(value, jsonTypeInfo);
Assert.Equal(expectedJson, json);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1809,7 +1809,7 @@ public static void GetTypeInfo_ImmutableOptionsInstance(Type type)
public static void GetTypeInfo_MutableOptions_CanModifyMetadata()
{
var options = new JsonSerializerOptions { TypeInfoResolver = new DefaultJsonTypeInfoResolver() };
JsonTypeInfo<TestClassForEncoding> jti = (JsonTypeInfo<TestClassForEncoding>)options.GetTypeInfo(typeof(TestClassForEncoding));
JsonTypeInfo<TestClassForEncoding> jti = options.GetTypeInfo<TestClassForEncoding>();

Assert.False(jti.IsReadOnly);
Assert.Equal(1, jti.Properties.Count);
Expand All @@ -1827,14 +1827,14 @@ public static void GetTypeInfo_MutableOptions_CanModifyMetadata()
Assert.Throws<InvalidOperationException>(() => options.IncludeFields = false);

// Getting JsonTypeInfo now should return a fresh immutable instance
JsonTypeInfo<TestClassForEncoding> jti2 = (JsonTypeInfo<TestClassForEncoding>)options.GetTypeInfo(typeof(TestClassForEncoding));
JsonTypeInfo<TestClassForEncoding> jti2 = options.GetTypeInfo<TestClassForEncoding>();
Assert.NotSame(jti, jti2);
Assert.True(jti2.IsReadOnly);
Assert.Equal(1, jti2.Properties.Count);
Assert.Throws<InvalidOperationException>(() => jti2.Properties.Clear());

// Subsequent requests return the same cached value
Assert.Same(jti2, options.GetTypeInfo(typeof(TestClassForEncoding)));
Assert.Same(jti2, options.GetTypeInfo<TestClassForEncoding>());

// Default contract should produce expected JSON
json = JsonSerializer.Serialize(value, options);
Expand Down Expand Up @@ -1923,7 +1923,7 @@ public static void GetTypeInfo_ResolverWithoutMetadata_ThrowsNotSupportedExcepti
public static void GetTypeInfo_ResultsAreGeneric<T>(T value, string expectedJson)
{
var options = new JsonSerializerOptions { TypeInfoResolver = new DefaultJsonTypeInfoResolver() };
JsonTypeInfo<T> jsonTypeInfo = (JsonTypeInfo<T>)options.GetTypeInfo(typeof(T));
JsonTypeInfo<T> jsonTypeInfo = options.GetTypeInfo<T>();
string json = JsonSerializer.Serialize(value, jsonTypeInfo);
Assert.Equal(expectedJson, json);
JsonSerializer.Deserialize(json, jsonTypeInfo);
Expand All @@ -1940,6 +1940,140 @@ public static IEnumerable<object[]> GetTypeInfo_ResultsAreGeneric_Values()
static object[] WrapArgs<T>(T value, string json) => new object[] { value, json };
}

[Fact]
public static void GetTypeInfoGeneric_MutableOptionsInstance()
{
var options = new JsonSerializerOptions();

// An unset resolver results in NotSupportedException.
Assert.Throws<NotSupportedException>(() => options.GetTypeInfo<int>());
Assert.False(options.TryGetTypeInfo<int>(out JsonTypeInfo<int>? typeInfo));
Assert.Null(typeInfo);

options.TypeInfoResolver = new DefaultJsonTypeInfoResolver();
JsonTypeInfo<int> typeInfo1 = options.GetTypeInfo<int>();
Assert.Equal(typeof(int), typeInfo1.Type);
Assert.False(typeInfo1.IsReadOnly);

JsonTypeInfo<int> typeInfo2 = options.GetTypeInfo<int>();
Assert.Equal(typeof(int), typeInfo2.Type);
Assert.False(typeInfo2.IsReadOnly);

Assert.NotSame(typeInfo1, typeInfo2);

Assert.True(options.TryGetTypeInfo<int>(out JsonTypeInfo<int>? typeInfo3));
Assert.Equal(typeof(int), typeInfo3.Type);
Assert.False(typeInfo3.IsReadOnly);

Assert.NotSame(typeInfo1, typeInfo3);

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

[Fact]
public static void GetTypeInfoGeneric_ImmutableOptionsInstance()
{
var options = new JsonSerializerOptions();
JsonSerializer.Serialize(42, options);

JsonTypeInfo<int> typeInfo = options.GetTypeInfo<int>();
Assert.Equal(typeof(int), typeInfo.Type);
Assert.True(typeInfo.IsReadOnly);

JsonTypeInfo<int> typeInfo2 = options.GetTypeInfo<int>();
Assert.Same(typeInfo, typeInfo2);

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

[Fact]
public static void GetTypeInfoGeneric_EquivalentToNonGenericVersion()
{
var options = new JsonSerializerOptions { TypeInfoResolver = new DefaultJsonTypeInfoResolver() };
JsonSerializer.Serialize(42, options); // Make options immutable to get caching

JsonTypeInfo nonGenericTypeInfo = options.GetTypeInfo(typeof(int));
JsonTypeInfo<int> genericTypeInfo = options.GetTypeInfo<int>();

Assert.Same(nonGenericTypeInfo, genericTypeInfo);

Assert.True(options.TryGetTypeInfo(typeof(string), out JsonTypeInfo? nonGenericTypeInfo2));
Assert.True(options.TryGetTypeInfo<string>(out JsonTypeInfo<string>? genericTypeInfo2));

Assert.Same(nonGenericTypeInfo2, genericTypeInfo2);
}

[Fact]
public static void GetTypeInfoGeneric_WorksWithVariousTypes()
{
var options = new JsonSerializerOptions { TypeInfoResolver = new DefaultJsonTypeInfoResolver() };

JsonTypeInfo<string> stringTypeInfo = options.GetTypeInfo<string>();
Assert.Equal(typeof(string), stringTypeInfo.Type);

JsonTypeInfo<List<int>> listTypeInfo = options.GetTypeInfo<List<int>>();
Assert.Equal(typeof(List<int>), listTypeInfo.Type);

JsonTypeInfo<Dictionary<string, object>> dictTypeInfo = options.GetTypeInfo<Dictionary<string, object>>();
Assert.Equal(typeof(Dictionary<string, object>), dictTypeInfo.Type);

JsonTypeInfo<object> objectTypeInfo = options.GetTypeInfo<object>();
Assert.Equal(typeof(object), objectTypeInfo.Type);
}

[Fact]
public static void GetTypeInfoGeneric_ResolverWithoutMetadata_ThrowsNotSupportedException()
{
var options = new JsonSerializerOptions();
options.AddContext<JsonContext>();

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

Assert.False(options.TryGetTypeInfo<BasicCompany>(out JsonTypeInfo<BasicCompany>? typeInfo));
Assert.Null(typeInfo);
}

[Fact]
public static void GetTypeInfoGeneric_MutableOptions_CanModifyMetadata()
{
var options = new JsonSerializerOptions { TypeInfoResolver = new DefaultJsonTypeInfoResolver() };
JsonTypeInfo<TestClassForEncoding> jti = options.GetTypeInfo<TestClassForEncoding>();

Assert.False(jti.IsReadOnly);
Assert.Equal(1, jti.Properties.Count);
jti.Properties.Clear();

var value = new TestClassForEncoding { MyString = "SomeValue" };
Assert.False(options.IsReadOnly);

string json = JsonSerializer.Serialize(value, jti);
Assert.Equal("{}", json);

// Using JsonTypeInfo will lock JsonSerializerOptions
Assert.True(options.IsReadOnly);
Assert.True(jti.IsReadOnly);
Assert.Throws<InvalidOperationException>(() => options.IncludeFields = false);

// Getting JsonTypeInfo now should return a fresh immutable instance
JsonTypeInfo<TestClassForEncoding> jti2 = options.GetTypeInfo<TestClassForEncoding>();
Assert.NotSame(jti, jti2);
Assert.True(jti2.IsReadOnly);
Assert.Equal(1, jti2.Properties.Count);
Assert.Throws<InvalidOperationException>(() => jti2.Properties.Clear());

// Subsequent requests return the same cached value
Assert.Same(jti2, options.GetTypeInfo<TestClassForEncoding>());

// Default contract should produce expected JSON
json = JsonSerializer.Serialize(value, options);
Assert.Equal("""{"MyString":"SomeValue"}""", json);

// Default contract should not impact contract of original JsonTypeInfo
json = JsonSerializer.Serialize(value, jti);
Assert.Equal("{}", json);
}

[Fact]
public static void AllowDuplicateProperties_RespectsSetting()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public async Task PrimitivesAsRootObject(object? value, string expectedJson)
Assert.Equal(expectedJson, json);

var options = new JsonSerializerOptions { TypeInfoResolver = new DefaultJsonTypeInfoResolver() };
JsonTypeInfo<object> objectTypeInfo = (JsonTypeInfo<object>)options.GetTypeInfo(typeof(object));
JsonTypeInfo<object> objectTypeInfo = options.GetTypeInfo<object>();
json = await Serializer.SerializeWrapper(value, objectTypeInfo);
Assert.Equal(expectedJson, json);
}
Expand Down
Loading