diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index c6c3d97c96ef4..fe799d389161e 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -364,6 +364,9 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { } [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("Getting a converter for a type may require reflection which depends on runtime code generation.")] [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; } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Getting a metadata for a type may require reflection which depends on unreferenced code.")] + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Getting a metadata for a type may require reflection which depends on runtime code generation.")] + public System.Text.Json.Serialization.Metadata.JsonTypeInfo GetTypeInfo(System.Type type) { throw null; } } public enum JsonTokenType : byte { @@ -1148,11 +1151,14 @@ public JsonPolymorphismOptions() { } public abstract partial class JsonPropertyInfo { internal JsonPropertyInfo() { } + public System.Reflection.ICustomAttributeProvider? AttributeProvider { get { throw null; } set { } } public System.Text.Json.Serialization.JsonConverter? CustomConverter { get { throw null; } set { } } public System.Func? Get { get { throw null; } set { } } + public bool IsExtensionData { get { throw null; } set { } } public string Name { get { throw null; } set { } } public System.Text.Json.Serialization.JsonNumberHandling? NumberHandling { get { throw null; } set { } } public System.Text.Json.JsonSerializerOptions Options { get { throw null; } } + public int Order { get { throw null; } set { } } public System.Type PropertyType { get { throw null; } } public System.Action? Set { get { throw null; } set { } } public System.Func? ShouldSerialize { get { throw null; } set { } } @@ -1183,6 +1189,10 @@ internal JsonTypeInfo() { } public System.Func? CreateObject { get { throw null; } set { } } public System.Text.Json.Serialization.Metadata.JsonTypeInfoKind Kind { get { throw null; } } public System.Text.Json.Serialization.JsonNumberHandling? NumberHandling { get { throw null; } set { } } + public System.Action? OnDeserialized { get { throw null; } set { } } + public System.Action? OnDeserializing { get { throw null; } set { } } + public System.Action? OnSerialized { get { throw null; } set { } } + public System.Action? OnSerializing { get { throw null; } set { } } public System.Text.Json.JsonSerializerOptions Options { get { throw null; } } public System.Text.Json.Serialization.Metadata.JsonPolymorphismOptions? PolymorphismOptions { get { throw null; } set { } } public System.Collections.Generic.IList Properties { get { throw null; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConfigurationList.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConfigurationList.cs index cbc8a3e74c6a0..fa8a7b1cc74e0 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConfigurationList.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConfigurationList.cs @@ -13,7 +13,7 @@ namespace System.Text.Json.Serialization /// internal abstract class ConfigurationList : IList { - private readonly List _list; + protected readonly List _list; public ConfigurationList(IList? source = null) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs index 5cd5bcd3c9b59..73e25b73fd2b6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs @@ -22,7 +22,7 @@ private static JsonTypeInfo GetTypeInfo(JsonSerializerOptions? options, Type run options ??= JsonSerializerOptions.Default; options.InitializeForReflectionSerializer(); - return options.GetJsonTypeInfoForRootType(runtimeType); + return options.GetTypeInfoForRootType(runtimeType); } private static JsonTypeInfo GetTypeInfo(JsonSerializerContext context, Type type) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs index 1b670c0a18dbe..7ad272ab340dc 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs @@ -23,10 +23,55 @@ public sealed partial class JsonSerializerOptions // Simple LRU cache for the public (de)serialize entry points that avoid some lookups in _cachingContext. private volatile JsonTypeInfo? _lastTypeInfo; + /// + /// Gets the contract metadata resolved by the current instance. + /// + /// The type to resolve contract metadata for. + /// The contract metadata resolved for . + /// + /// Returned metadata can be downcast to and used with the relevant overloads. + /// + /// If the instance is locked for modification, the method will return a cached instance for the metadata. + /// + [RequiresUnreferencedCode("Getting a metadata for a type may require reflection which depends on unreferenced code.")] + [RequiresDynamicCode("Getting a metadata for a type may require reflection which depends on runtime code generation.")] + public JsonTypeInfo GetTypeInfo(Type type) + { + if (type is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(type)); + } + + if (JsonTypeInfo.IsInvalidForSerialization(type)) + { + ThrowHelper.ThrowArgumentException_CannotSerializeInvalidType(nameof(type), type, null, null); + } + + _typeInfoResolver ??= DefaultJsonTypeInfoResolver.RootDefaultInstance(); + + JsonTypeInfo? typeInfo; + if (IsLockedInstance) + { + typeInfo = GetCachingContext()?.GetOrAddJsonTypeInfo(type); + typeInfo?.EnsureConfigured(); + } + else + { + typeInfo = GetTypeInfoNoCaching(type); + } + + if (typeInfo is null) + { + ThrowHelper.ThrowNotSupportedException_NoMetadataForType(type); + } + + return typeInfo; + } + /// /// This method returns configured non-null JsonTypeInfo /// - internal JsonTypeInfo GetJsonTypeInfoCached(Type type) + internal JsonTypeInfo GetTypeInfoCached(Type type) { JsonTypeInfo? typeInfo = null; @@ -45,7 +90,7 @@ internal JsonTypeInfo GetJsonTypeInfoCached(Type type) return typeInfo; } - internal bool TryGetJsonTypeInfoCached(Type type, [NotNullWhen(true)] out JsonTypeInfo? typeInfo) + internal bool TryGetTypeInfoCached(Type type, [NotNullWhen(true)] out JsonTypeInfo? typeInfo) { if (_cachingContext == null) { @@ -61,13 +106,13 @@ internal bool TryGetJsonTypeInfoCached(Type type, [NotNullWhen(true)] out JsonTy /// This has an LRU cache that is intended only for public API calls that specify the root type. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal JsonTypeInfo GetJsonTypeInfoForRootType(Type type) + internal JsonTypeInfo GetTypeInfoForRootType(Type type) { JsonTypeInfo? jsonTypeInfo = _lastTypeInfo; if (jsonTypeInfo?.Type != type) { - jsonTypeInfo = GetJsonTypeInfoCached(type); + jsonTypeInfo = GetTypeInfoCached(type); _lastTypeInfo = jsonTypeInfo; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Converters.cs index dc8fbc5e188ff..690281c6e0949 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Converters.cs @@ -144,7 +144,7 @@ internal static JsonConverter GetConverterForMember( JsonConverterAttribute? converterAttribute = memberInfo.GetUniqueCustomAttribute(inherit: false); customConverter = converterAttribute is null ? null : GetConverterFromAttribute(converterAttribute, typeToConvert, memberInfo, options); - return options.TryGetJsonTypeInfoCached(typeToConvert, out JsonTypeInfo? typeInfo) + return options.TryGetTypeInfoCached(typeToConvert, out JsonTypeInfo? typeInfo) ? typeInfo.Converter : GetConverterForType(typeToConvert, options); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs index 9a262ed3648e1..76866852d2bc5 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs @@ -39,7 +39,7 @@ public JsonTypeInfo JsonTypeInfo { Debug.Assert(Options != null); Debug.Assert(ShouldDeserialize); - return _jsonTypeInfo ??= Options.GetJsonTypeInfoCached(PropertyType); + return _jsonTypeInfo ??= Options.GetTypeInfoCached(PropertyType); } set { @@ -97,7 +97,7 @@ public static JsonParameterInfo CreateIgnoredParameterPlaceholder( Type parameterType = parameterInfo.ParameterType; DefaultValueHolder holder; - if (matchingProperty.Options.TryGetJsonTypeInfoCached(parameterType, out JsonTypeInfo? typeInfo)) + if (matchingProperty.Options.TryGetTypeInfoCached(parameterType, out JsonTypeInfo? typeInfo)) { holder = typeInfo.DefaultValueHolder; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs index 857e0f86aad24..70ab27004493c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs @@ -143,7 +143,7 @@ internal JsonIgnoreCondition? IgnoreCondition /// Setting a custom attribute provider will have no impact on the contract model, /// but serves as metadata for downstream contract modifiers. /// - internal ICustomAttributeProvider? AttributeProvider + public ICustomAttributeProvider? AttributeProvider { get => _attributeProvider; set @@ -166,7 +166,7 @@ internal ICustomAttributeProvider? AttributeProvider /// Properties annotated with /// will appear here when using or . /// - internal bool IsExtensionData + public bool IsExtensionData { get => _isExtensionDataProperty; set @@ -631,7 +631,7 @@ public string Name /// When using , properties annotated /// with the will map to this value. /// - internal int Order + public int Order { get => _order; set @@ -757,7 +757,7 @@ internal JsonTypeInfo JsonTypeInfo else { // GetOrAddJsonTypeInfo already ensures it's configured. - _jsonTypeInfo = Options.GetJsonTypeInfoCached(PropertyType); + _jsonTypeInfo = Options.GetTypeInfoCached(PropertyType); } return _jsonTypeInfo; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index 861b693b27e3b..1d3ecb5f6c74e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -53,7 +53,7 @@ public Func? CreateObject /// /// Types implementing will map to this callback. /// - internal Action? OnSerializing + public Action? OnSerializing { get => _onSerializing; set @@ -75,7 +75,7 @@ internal Action? OnSerializing /// /// Types implementing will map to this callback. /// - internal Action? OnSerialized + public Action? OnSerialized { get => _onSerialized; set @@ -97,7 +97,7 @@ internal Action? OnSerialized /// /// Types implementing will map to this callback. /// - internal Action? OnDeserializing + public Action? OnDeserializing { get => _onDeserializing; set @@ -119,7 +119,7 @@ internal Action? OnDeserializing /// /// Types implementing will map to this callback. /// - internal Action? OnDeserialized + public Action? OnDeserialized { get => _onDeserialized; set @@ -226,7 +226,7 @@ internal JsonTypeInfo? ElementTypeInfo { // GetOrAddJsonTypeInfo already ensures JsonTypeInfo is configured // also see comment on JsonPropertyInfo.JsonTypeInfo - _elementTypeInfo = Options.GetJsonTypeInfoCached(ElementType); + _elementTypeInfo = Options.GetTypeInfoCached(ElementType); } } else @@ -268,7 +268,7 @@ internal JsonTypeInfo? KeyTypeInfo // GetOrAddJsonTypeInfo already ensures JsonTypeInfo is configured // also see comment on JsonPropertyInfo.JsonTypeInfo - _keyTypeInfo = Options.GetJsonTypeInfoCached(KeyType); + _keyTypeInfo = Options.GetTypeInfoCached(KeyType); } } else @@ -735,7 +735,7 @@ internal void InitializePropertyCache() ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameConflict(Type, property.Name); } - isOrderSpecified = property.Order != 0; + isOrderSpecified |= property.Order != 0; } if (isOrderSpecified) @@ -934,7 +934,7 @@ public JsonPropertyInfoList(JsonTypeInfo jsonTypeInfo) { if (jsonTypeInfo.ExtensionDataProperty is not null) { - Add(jsonTypeInfo.ExtensionDataProperty); + _list.Add(jsonTypeInfo.ExtensionDataProperty); } _jsonTypeInfo = jsonTypeInfo; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PolymorphicTypeResolver.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PolymorphicTypeResolver.cs index c2a73ea6ee928..b85200a2ca143 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PolymorphicTypeResolver.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PolymorphicTypeResolver.cs @@ -249,7 +249,7 @@ public DerivedJsonTypeInfo(Type type, object? typeDiscriminator) public Type DerivedType { get; } public object? TypeDiscriminator { get; } public JsonTypeInfo GetJsonTypeInfo(JsonSerializerOptions options) - => _jsonTypeInfo ??= options.GetJsonTypeInfoCached(DerivedType); + => _jsonTypeInfo ??= options.GetTypeInfoCached(DerivedType); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs index 5ff6e12662862..b711b3d303d06 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs @@ -95,7 +95,7 @@ private void EnsurePushCapacity() public void Initialize(Type type, JsonSerializerOptions options, bool supportContinuation) { - JsonTypeInfo jsonTypeInfo = options.GetJsonTypeInfoForRootType(type); + JsonTypeInfo jsonTypeInfo = options.GetTypeInfoForRootType(type); Initialize(jsonTypeInfo, supportContinuation); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs index f3fbe1a7ad817..f1e6c02461085 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs @@ -138,7 +138,7 @@ private void EnsurePushCapacity() /// public JsonConverter Initialize(Type type, JsonSerializerOptions options, bool supportContinuation, bool supportAsync) { - JsonTypeInfo jsonTypeInfo = options.GetJsonTypeInfoForRootType(type); + JsonTypeInfo jsonTypeInfo = options.GetTypeInfoForRootType(type); return Initialize(jsonTypeInfo, supportContinuation, supportAsync); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs index 43bf3ef4daa22..705fe03d7f5d5 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs @@ -126,7 +126,7 @@ public JsonConverter InitializePolymorphicReEntry(Type runtimeType, JsonSerializ // if the current element is the same type as the previous element. if (PolymorphicJsonTypeInfo?.PropertyType != runtimeType) { - JsonTypeInfo typeInfo = options.GetJsonTypeInfoCached(runtimeType); + JsonTypeInfo typeInfo = options.GetTypeInfoCached(runtimeType); PolymorphicJsonTypeInfo = typeInfo.PropertyInfoForTypeInfo; } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonPropertyInfo.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonPropertyInfo.cs index 91d47049b9543..3642e9459f657 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonPropertyInfo.cs @@ -1,13 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Reflection; -using System.Text; +using System.Text.Json.Nodes; +using System.Text.Json.Nodes.Tests; using System.Text.Json.Serialization.Metadata; using System.Text.Json.Tests; -using System.Threading.Tasks; using Xunit; namespace System.Text.Json.Serialization.Tests @@ -1178,5 +1179,270 @@ private class MyClassCustomConverterFactory : JsonConverterFactory return ConverterInstance; } } + + [Fact] + public static void ClassWithExtensionDataAttribute_IsReturnedInMetadata() + { + var resolver = new DefaultJsonTypeInfoResolver(); + var jti = resolver.GetTypeInfo(typeof(ClassWithExtensionDataAttribute), new()); + + Assert.Equal(3, jti.Properties.Count); + Assert.False(jti.Properties[0].IsExtensionData); + Assert.False(jti.Properties[1].IsExtensionData); + + JsonPropertyInfo lastProperty = jti.Properties[2]; + Assert.Equal("ExtensionData", lastProperty.Name); + Assert.Equal(typeof(Dictionary), lastProperty.PropertyType); + Assert.True(lastProperty.IsExtensionData); + } + + [Fact] + public static void ClassWithExtensionDataAttribute_ChangingExtensionDataFlagDisablesExtensionData() + { + var options = new JsonSerializerOptions + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver() + { + Modifiers = + { + jti => + { + if (jti.Type == typeof(ClassWithExtensionDataAttribute)) + { + Assert.Equal(3, jti.Properties.Count); + Assert.True(jti.Properties[2].IsExtensionData); + jti.Properties[2].IsExtensionData = false; + } + } + } + } + }; + + var value = new ClassWithExtensionDataAttribute + { + Value1 = 1, + Value2 = 2, + ExtensionData = new Dictionary + { + ["Value3"] = 3 + } + }; + + string json = JsonSerializer.Serialize(value, options); + JsonTestHelper.AssertJsonEqual("""{"Value1":1,"Value2":2,"ExtensionData":{"Value3":3}}""", json); + + value = JsonSerializer.Deserialize("""{"unrecognizedValue":42}""", options); + Assert.Null(value.ExtensionData); + } + + [Fact] + public static void ClassWithExtensionDataAttribute_RemovingExtensionDataPropertyIgnoresExtensionData() + { + var options = new JsonSerializerOptions + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver() + { + Modifiers = + { + jti => + { + if (jti.Type == typeof(ClassWithExtensionDataAttribute)) + { + Assert.Equal(3, jti.Properties.Count); + Assert.True(jti.Properties[2].IsExtensionData); + jti.Properties.RemoveAt(2); + } + } + } + } + }; + + var value = new ClassWithExtensionDataAttribute + { + Value1 = 1, + Value2 = 2, + ExtensionData = new Dictionary + { + ["Value3"] = 3 + } + }; + + string json = JsonSerializer.Serialize(value, options); + JsonTestHelper.AssertJsonEqual("""{"Value1":1,"Value2":2}""", json); + + value = JsonSerializer.Deserialize("""{"unrecognizedValue":42}""", options); + Assert.Null(value.ExtensionData); + } + + [Theory] + [InlineData(typeof(IDictionary))] + [InlineData(typeof(IDictionary))] + [InlineData(typeof(Dictionary))] + [InlineData(typeof(Dictionary))] + [InlineData(typeof(ConcurrentDictionary))] + [InlineData(typeof(JsonObject))] + public static void EnablingExtensionData_SupportedPropertyType(Type type) + { + var jti = JsonTypeInfo.CreateJsonTypeInfo(typeof(ClassWithExtensionDataAttribute), new()); + var jpi = jti.CreateJsonPropertyInfo(type, "ExtensionData"); + + Assert.False(jpi.IsExtensionData); + jpi.IsExtensionData = true; + Assert.True(jpi.IsExtensionData); + } + + [Theory] + [InlineData(typeof(int))] + [InlineData(typeof(string))] + [InlineData(typeof(object))] + [InlineData(typeof(ClassWithExtensionDataAttribute))] + [InlineData(typeof(List))] + [InlineData(typeof(IDictionary))] + [InlineData(typeof(Dictionary))] + public static void EnablingExtensionData_UnsupportedPropertyType_ThrowsInvalidOperationException(Type type) + { + var jti = JsonTypeInfo.CreateJsonTypeInfo(typeof(ClassWithExtensionDataAttribute), new()); + var jpi = jti.CreateJsonPropertyInfo(type, "ExtensionData"); + + Assert.False(jpi.IsExtensionData); + Assert.Throws(() => jpi.IsExtensionData = true); + } + + public class ClassWithExtensionDataAttribute + { + public int Value1 { get; set; } + public int Value2 { get; set; } + + [JsonExtensionData] + public Dictionary ExtensionData { get; set; } + } + + [Fact] + public static void ClassWithoutExtensionDataAttribute_CanHaveExtensionDataEnabled() + { + var options = new JsonSerializerOptions + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver() + { + Modifiers = + { + jti => + { + if (jti.Type == typeof(ClassWithTwoExtensionDataLikeProperties)) + { + JsonPropertyInfo propertyInfo = jti.Properties.First(prop => prop.Name == "ExtensionData2"); + propertyInfo.IsExtensionData = true; + } + } + } + } + }; + + var value = new ClassWithTwoExtensionDataLikeProperties + { + ExtensionData1 = new Dictionary { ["extension1"] = 1 }, + ExtensionData2 = new Dictionary { ["extension2"] = 2 }, + }; + + string json = JsonSerializer.Serialize(value, options); + JsonTestHelper.AssertJsonEqual("""{"ExtensionData1":{"extension1":1},"extension2":2}""", json); + + value = JsonSerializer.Deserialize("""{"unrecognizedData":3}""", options); + Assert.Null(value.ExtensionData1); + Assert.NotNull(value.ExtensionData2); + Assert.True(value.ExtensionData2.ContainsKey("unrecognizedData")); + } + + [Fact] + public static void ClassWithoutExtensionDataAttribute_SettingDuplicateExtensionDataProperties_ThrowsInvalidOperationException() + { + bool resolverRanToCompletion = false; + var options = new JsonSerializerOptions + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver() + { + Modifiers = + { + jti => + { + if (jti.Type == typeof(ClassWithTwoExtensionDataLikeProperties)) + { + Assert.Equal(2, jti.Properties.Count); + jti.Properties[0].IsExtensionData = true; + jti.Properties[1].IsExtensionData = true; + resolverRanToCompletion = true; + } + } + } + } + }; + + var value = new ClassWithTwoExtensionDataLikeProperties(); + Assert.Throws(() => JsonSerializer.Serialize(value, options)); + Assert.True(resolverRanToCompletion); + } + + public class ClassWithTwoExtensionDataLikeProperties + { + public Dictionary ExtensionData1 { get; set; } + public Dictionary ExtensionData2 { get; set; } + } + + [Fact] + public static void DefaultJsonTypeInfoResolver_JsonPropertyInfo_ReturnsMemberInfoAsAttributeProvider() + { + var resolver = new DefaultJsonTypeInfoResolver(); + JsonTypeInfo jti = resolver.GetTypeInfo(typeof(ClassWithFieldsAndProperties), new()); + + Assert.Equal(2, jti.Properties.Count); + + JsonPropertyInfo fieldPropInfo = jti.Properties.First(prop => prop.Name == "Field"); + FieldInfo fieldInfo = typeof(ClassWithFieldsAndProperties).GetField("Field"); + Assert.NotNull(fieldInfo); + Assert.Same(fieldInfo, fieldPropInfo.AttributeProvider); + + JsonPropertyInfo propertyPropInfo = jti.Properties.First(prop => prop.Name == "Property"); + PropertyInfo propInfo = typeof(ClassWithFieldsAndProperties).GetProperty("Property"); + Assert.NotNull(propInfo); + Assert.Same(propInfo, propertyPropInfo.AttributeProvider); + } + + [Fact] + public static void JsonPropertyInfo_AttributeProvider_ReassigningValuesHasNoEffect() + { + var options = new JsonSerializerOptions + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver() + { + Modifiers = + { + jti => + { + if (jti.Type == typeof(ClassWithFieldsAndProperties)) + { + Assert.Equal(2, jti.Properties.Count); + + jti.Properties[0].AttributeProvider = jti.Properties[1].AttributeProvider; + Assert.Same(jti.Properties[0].AttributeProvider, jti.Properties[1].AttributeProvider); + + jti.Properties[1].AttributeProvider = null; + Assert.Null(jti.Properties[1].AttributeProvider); + } + } + } + } + }; + + var value = new ClassWithFieldsAndProperties { Field = "Field", Property = 42 }; + string json = JsonSerializer.Serialize(value, options); + JsonTestHelper.AssertJsonEqual("""{"Field":"Field","Property":42}""", json); + } + + public class ClassWithFieldsAndProperties + { + [JsonInclude] + public string Field; + public int Property { get; set; } + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonTypeInfo.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonTypeInfo.cs index 8db7c72e00e96..d7d6013b81bca 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonTypeInfo.cs @@ -1,14 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text; using System.Text.Json.Serialization.Metadata; using System.Text.Json.Tests; -using System.Threading.Tasks; using Xunit; namespace System.Text.Json.Serialization.Tests @@ -920,17 +917,27 @@ public static void PropertyOrderIsRespected() if (ti.Type == typeof(ClassWithExplicitOrderOfProperties)) { Assert.Equal(5, ti.Properties.Count); + Assert.Equal("A", ti.Properties[0].Name); Assert.Equal("B", ti.Properties[1].Name); Assert.Equal("C", ti.Properties[2].Name); Assert.Equal("D", ti.Properties[3].Name); Assert.Equal("E", ti.Properties[4].Name); - // swapping A,B (both with Order property) - (ti.Properties[0], ti.Properties[1]) = (ti.Properties[1], ti.Properties[0]); + Assert.Equal(-2, ti.Properties[0].Order); + Assert.Equal(-1, ti.Properties[1].Order); + Assert.Equal(0, ti.Properties[2].Order); + Assert.Equal(1, ti.Properties[3].Order); + Assert.Equal(2, ti.Properties[4].Order); + + // swapping A,B order values + (ti.Properties[0].Order, ti.Properties[1].Order) = (ti.Properties[1].Order, ti.Properties[0].Order); + + // swapping E,C order values + (ti.Properties[2].Order, ti.Properties[4].Order) = (ti.Properties[4].Order, ti.Properties[2].Order); - // swapping E,C (one with Order property one without) - (ti.Properties[2], ti.Properties[4]) = (ti.Properties[4], ti.Properties[2]); + // swapping B,D properties (has no effect on contract) + (ti.Properties[1], ti.Properties[3]) = (ti.Properties[3], ti.Properties[1]); } } } @@ -953,9 +960,11 @@ public static void PropertyOrderIsRespected() private class ClassWithExplicitOrderOfProperties { public string C { get; set; } - public string D { get; set; } [JsonPropertyOrder(1)] + public string D { get; set; } + + [JsonPropertyOrder(2)] public string E { get; set; } [JsonPropertyOrder(-1)] @@ -1128,5 +1137,106 @@ public class CustomConverter : JsonConverter public override void Write(Utf8JsonWriter writer, ClassWithConverterAttribute value, JsonSerializerOptions options) => throw new NotImplementedException(); } } + + [Fact] + public static void ClassWithCallBacks_JsonTypeInfoCallbackDelegatesArePopulated() + { + var resolver = new DefaultJsonTypeInfoResolver(); + var jti = resolver.GetTypeInfo(typeof(ClassWithCallBacks), new()); + + Assert.NotNull(jti.OnSerializing); + Assert.NotNull(jti.OnSerialized); + Assert.NotNull(jti.OnDeserializing); + Assert.NotNull(jti.OnDeserialized); + + var value = new ClassWithCallBacks(); + jti.OnSerializing(value); + Assert.Equal(1, value.IsOnSerializingInvocations); + + jti.OnSerialized(value); + Assert.Equal(1, value.IsOnSerializedInvocations); + + jti.OnDeserializing(value); + Assert.Equal(1, value.IsOnDeserializingInvocations); + + jti.OnDeserialized(value); + Assert.Equal(1, value.IsOnDeserializedInvocations); + } + + [Fact] + public static void ClassWithCallBacks_CanCustomizeCallbacks() + { + var options = new JsonSerializerOptions + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static jti => + { + if (jti.Type == typeof(ClassWithCallBacks)) + { + jti.OnSerializing = null; + jti.OnSerialized = (obj => ((ClassWithCallBacks)obj).IsOnSerializedInvocations += 10); + + jti.OnDeserializing = null; + jti.OnDeserialized = (obj => ((ClassWithCallBacks)obj).IsOnDeserializedInvocations += 7); + } + } + } + } + }; + + var value = new ClassWithCallBacks(); + string json = JsonSerializer.Serialize(value, options); + Assert.Equal("{}", json); + + Assert.Equal(0, value.IsOnSerializingInvocations); + Assert.Equal(10, value.IsOnSerializedInvocations); + + value = JsonSerializer.Deserialize(json, options); + Assert.Equal(0, value.IsOnDeserializingInvocations); + Assert.Equal(7, value.IsOnDeserializedInvocations); + } + + [Theory] + [InlineData(typeof(int))] + [InlineData(typeof(string))] + [InlineData(typeof(object))] + [InlineData(typeof(List))] + [InlineData(typeof(Dictionary))] + public static void SettingCallbacksOnUnsupportedTypes_ThrowsInvalidOperationException(Type type) + { + var jti = JsonTypeInfo.CreateJsonTypeInfo(type, new()); + + Assert.NotEqual(JsonTypeInfoKind.Object, jti.Kind); + Assert.Throws(() => jti.OnSerializing = null); + Assert.Throws(() => jti.OnSerializing = (obj => { })); + Assert.Throws(() => jti.OnSerialized = null); + Assert.Throws(() => jti.OnSerialized = (obj => { })); + Assert.Throws(() => jti.OnDeserializing = null); + Assert.Throws(() => jti.OnDeserializing = (obj => { })); + Assert.Throws(() => jti.OnDeserialized = null); + Assert.Throws(() => jti.OnDeserialized = (obj => { })); + } + + public class ClassWithCallBacks : + IJsonOnSerializing, IJsonOnSerialized, + IJsonOnDeserializing, IJsonOnDeserialized + { + [JsonIgnore] + public int IsOnSerializingInvocations { get; set; } + [JsonIgnore] + public int IsOnSerializedInvocations { get; set; } + [JsonIgnore] + public int IsOnDeserializingInvocations { get; set; } + [JsonIgnore] + public int IsOnDeserializedInvocations { get; set; } + + public void OnSerializing() => IsOnSerializingInvocations++; + public void OnSerialized() => IsOnSerializedInvocations++; + public void OnDeserializing() => IsOnDeserializingInvocations++; + public void OnDeserialized() => IsOnDeserializedInvocations++; + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs index 40764bf7d5859..a58cdea5678c8 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs @@ -9,6 +9,7 @@ using System.Text.Json.Serialization.Metadata; using System.Text.Json.Tests; using System.Text.Unicode; +using System.Threading; using Microsoft.DotNet.RemoteExecutor; using Xunit; @@ -918,5 +919,165 @@ public static void ConverterRead_VerifyInvalidTypeToConvertFails() } } } + + [Theory] + [InlineData(typeof(int))] + [InlineData(typeof(object))] + [InlineData(typeof(List))] + [InlineData(typeof(Dictionary))] + public static void GetTypeInfo_MutableOptionsInstance(Type type) + { + var options = new JsonSerializerOptions(); + JsonTypeInfo typeInfo = options.GetTypeInfo(type); + Assert.Equal(type, typeInfo.Type); + + JsonTypeInfo typeInfo2 = options.GetTypeInfo(type); + Assert.Equal(type, typeInfo2.Type); + + Assert.NotSame(typeInfo, typeInfo2); + + options.WriteIndented = true; // can mutate without issue + } + + [Theory] + [InlineData(typeof(int))] + [InlineData(typeof(object))] + [InlineData(typeof(List))] + [InlineData(typeof(Dictionary))] + public static void GetTypeInfo_ImmutableOptionsInstance(Type type) + { + var options = new JsonSerializerOptions(); + JsonSerializer.Serialize(42, options); + + JsonTypeInfo typeInfo = options.GetTypeInfo(type); + Assert.Equal(type, typeInfo.Type); + + JsonTypeInfo typeInfo2 = options.GetTypeInfo(type); + Assert.Same(typeInfo, typeInfo2); + } + + [Fact] + public static void GetTypeInfo_MutableOptions_CanModifyMetadata() + { + var options = new JsonSerializerOptions(); + JsonTypeInfo jti = (JsonTypeInfo)options.GetTypeInfo(typeof(TestClassForEncoding)); + + Assert.Equal(1, jti.Properties.Count); + jti.Properties.Clear(); + + var value = new TestClassForEncoding { MyString = "SomeValue" }; + string json = JsonSerializer.Serialize(value, jti); + Assert.Equal("{}", json); + + // Using JsonTypeInfo will lock JsonSerializerOptions + Assert.Throws(() => options.IncludeFields = false); + + // Getting JsonTypeInfo now should return a fresh immutable instance + JsonTypeInfo jti2 = (JsonTypeInfo)options.GetTypeInfo(typeof(TestClassForEncoding)); + Assert.NotSame(jti, jti2); + Assert.Equal(1, jti2.Properties.Count); + Assert.Throws(() => jti2.Properties.Clear()); + + // Subsequent requests return the same cached value + Assert.Same(jti2, options.GetTypeInfo(typeof(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 GetTypeInfo_NullInput_ThrowsArgumentNullException() + { + var options = new JsonSerializerOptions(); + Assert.Throws(() => options.GetTypeInfo(null)); + } + + [Fact] + public static void GetTypeInfo_RecursiveResolver_StackOverflows() + { + var resolver = new RecursiveResolver(); + var options = new JsonSerializerOptions { TypeInfoResolver = resolver }; + + Assert.Throws(() => options.GetTypeInfo(typeof(TestClassForEncoding))); + Assert.True(resolver.IsThresholdReached); + } + + private class RecursiveResolver : IJsonTypeInfoResolver + { + private const int MaxDepth = 10; + + [ThreadStatic] + private int _isResolverEntered = 0; + + public bool IsThresholdReached { get; private set; } + + public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) + { + if (_isResolverEntered == MaxDepth) + { + IsThresholdReached = true; + return null; + } + + _isResolverEntered++; + try + { + return options.GetTypeInfo(type); + } + finally + { + _isResolverEntered--; + } + } + } + + [Theory] + [InlineData(typeof(void))] + [InlineData(typeof(Dictionary<,>))] + [InlineData(typeof(List<>))] + [InlineData(typeof(Nullable<>))] + [InlineData(typeof(int*))] + [InlineData(typeof(Span))] + public static void GetTypeInfo_InvalidInput_ThrowsArgumentException(Type type) + { + var options = new JsonSerializerOptions(); + Assert.Throws(() => options.GetTypeInfo(type)); + } + + [Fact] + public static void GetTypeInfo_ResolverWithoutMetadata_ThrowsNotSupportedException() + { + var options = new JsonSerializerOptions(); + options.AddContext(); + + Assert.Throws(() => options.GetTypeInfo(typeof(BasicCompany))); + } + + [Theory] + [MemberData(nameof(GetTypeInfo_ResultsAreGeneric_Values))] + public static void GetTypeInfo_ResultsAreGeneric(T value, string expectedJson) + { + var options = new JsonSerializerOptions(); + JsonTypeInfo jsonTypeInfo = (JsonTypeInfo)options.GetTypeInfo(typeof(T)); + string json = JsonSerializer.Serialize(value, jsonTypeInfo); + Assert.Equal(expectedJson, json); + JsonSerializer.Deserialize(json, jsonTypeInfo); + } + + public static IEnumerable GetTypeInfo_ResultsAreGeneric_Values() + { + yield return WrapArgs(42, "42"); + yield return WrapArgs("string", "\"string\""); + yield return WrapArgs(new { Value = 42, String = "str" }, """{"Value":42,"String":"str"}"""); + yield return WrapArgs(new List { 1, 2, 3, 4, 5 }, """[1,2,3,4,5]"""); + yield return WrapArgs(new Dictionary { ["key"] = 42 }, """{"key":42}"""); + + static object[] WrapArgs(T value, string json) => new object[] { value, json }; + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TypeInfoResolverFunctionalTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TypeInfoResolverFunctionalTests.cs index 3924e8f1e83bd..770de5693ff47 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TypeInfoResolverFunctionalTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TypeInfoResolverFunctionalTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Linq; using System.Reflection; using System.Runtime.Serialization; using System.Text.Json.Serialization.Metadata; @@ -697,24 +696,25 @@ public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions option if (jsonTypeInfo.Kind == JsonTypeInfoKind.Object && type.GetCustomAttribute() is not null) { - jsonTypeInfo.Properties.Clear(); // TODO should not require clearing - - IEnumerable<(PropertyInfo propInfo, DataMemberAttribute attr)> properties = type - .GetProperties(BindingFlags.Instance | BindingFlags.Public) - .Where(propInfo => propInfo.GetCustomAttribute() is null) - .Select(propInfo => (propInfo, attr: propInfo.GetCustomAttribute())) - .OrderBy(entry => entry.attr?.Order ?? 0); + jsonTypeInfo.Properties.Clear(); - foreach ((PropertyInfo propertyInfo, DataMemberAttribute? attr) in properties) + foreach (PropertyInfo propInfo in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)) { - JsonPropertyInfo jsonPropertyInfo = jsonTypeInfo.CreateJsonPropertyInfo(propertyInfo.PropertyType, attr?.Name ?? propertyInfo.Name); + if (propInfo.GetCustomAttribute() is not null) + { + continue; + } + + DataMemberAttribute? attr = propInfo.GetCustomAttribute(); + JsonPropertyInfo jsonPropertyInfo = jsonTypeInfo.CreateJsonPropertyInfo(propInfo.PropertyType, attr?.Name ?? propInfo.Name); + jsonPropertyInfo.Order = attr?.Order ?? 0; jsonPropertyInfo.Get = - propertyInfo.CanRead - ? propertyInfo.GetValue + propInfo.CanRead + ? propInfo.GetValue : null; - jsonPropertyInfo.Set = propertyInfo.CanWrite - ? propertyInfo.SetValue + jsonPropertyInfo.Set = propInfo.CanWrite + ? propInfo.SetValue : null; jsonTypeInfo.Properties.Add(jsonPropertyInfo);