Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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,47 @@ 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>()
{
if (JsonTypeInfo.IsInvalidForSerialization(typeof(T)))
{
ThrowHelper.ThrowArgumentException_CannotSerializeInvalidType(nameof(T), typeof(T), null, null);
}

return (JsonTypeInfo<T>)GetTypeInfoInternal(typeof(T), resolveIfMutable: true);
}
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)
{
if (JsonTypeInfo.IsInvalidForSerialization(typeof(T)))
{
ThrowHelper.ThrowArgumentException_CannotSerializeInvalidType(nameof(T), typeof(T), null, null);
}

JsonTypeInfo? info = GetTypeInfoInternal(typeof(T), ensureNotNull: null, resolveIfMutable: true);
typeInfo = (JsonTypeInfo<T>?)info;
return typeInfo is not null;
Comment thread
stephentoub marked this conversation as resolved.
Outdated
}

/// <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 @@ -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
Loading