diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index febf787afeef8..62e9fd57d8299 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -20,6 +20,7 @@ private sealed partial class Emitter private const string CreateValueInfoMethodName = "CreateValueInfo"; private const string CtorParamInitMethodNameSuffix = "CtorParamInit"; private const string DefaultOptionsStaticVarName = "s_defaultOptions"; + private const string InstanceMemberBindingFlagsVariableName = "InstanceMemberBindingFlags"; private const string OriginatingResolverPropertyName = "OriginatingResolver"; private const string InfoVarName = "info"; private const string NumberHandlingPropName = "NumberHandling"; @@ -500,6 +501,7 @@ private SourceText GenerateForObject(ContextGenerationSpec contextSpec, TypeGene string? propInitMethodName = null; string? propInitAdapterFunc = null; + string? constructorInfoFactoryFunc = null; string? ctorParamMetadataInitMethodName = null; string? serializeMethodName = null; @@ -508,10 +510,20 @@ private SourceText GenerateForObject(ContextGenerationSpec contextSpec, TypeGene propInitMethodName = $"{typeFriendlyName}{PropInitMethodNameSuffix}"; propInitAdapterFunc = $"_ => {propInitMethodName}({OptionsLocalVariableName})"; - if (constructionStrategy == ObjectConstructionStrategy.ParameterizedConstructor) + if (constructionStrategy is ObjectConstructionStrategy.ParameterizedConstructor) { ctorParamMetadataInitMethodName = $"{typeFriendlyName}{CtorParamInitMethodNameSuffix}"; } + + if (constructionStrategy is ObjectConstructionStrategy.ParameterlessConstructor + or ObjectConstructionStrategy.ParameterizedConstructor) + { + string argTypes = typeMetadata.CtorParamGenSpecs.Count == 0 + ? "global::System.Array.Empty()" + : $$"""new[] {{{string.Join(", ", typeMetadata.CtorParamGenSpecs.Select(p => $"typeof({p.ParameterType.FullyQualifiedName})"))}}}"""; + + constructorInfoFactoryFunc = $"static () => typeof({typeMetadata.TypeRef.FullyQualifiedName}).GetConstructor({InstanceMemberBindingFlagsVariableName}, binder: null, {argTypes}, modifiers: null)"; + } } if (ShouldGenerateSerializationLogic(typeMetadata)) @@ -531,7 +543,8 @@ private SourceText GenerateForObject(ContextGenerationSpec contextSpec, TypeGene ObjectWithParameterizedConstructorCreator = {{parameterizedCreatorInvocation}}, PropertyMetadataInitializer = {{propInitAdapterFunc ?? "null"}}, ConstructorParameterMetadataInitializer = {{ctorParamMetadataInitMethodName ?? "null"}}, - {{SerializeHandlerPropName}} = {{serializeMethodName ?? "null"}} + ConstructorAttributeProviderFactory = {{constructorInfoFactoryFunc ?? "null"}}, + {{SerializeHandlerPropName}} = {{serializeMethodName ?? "null"}}, }; {{JsonTypeInfoLocalVariableName}} = {{JsonMetadataServicesTypeRef}}.CreateObjectInfo<{{typeMetadata.TypeRef.FullyQualifiedName}}>({{OptionsLocalVariableName}}, {{ObjectInfoVarName}}); @@ -651,7 +664,8 @@ private void GeneratePropMetadataInitFunc(SourceWriter writer, string propInitMe IsExtensionData = {{FormatBoolLiteral(property.IsExtensionData)}}, NumberHandling = {{FormatNumberHandling(property.NumberHandling)}}, PropertyName = {{FormatStringLiteral(property.MemberName)}}, - JsonPropertyName = {{FormatStringLiteral(property.JsonPropertyName)}} + JsonPropertyName = {{FormatStringLiteral(property.JsonPropertyName)}}, + AttributeProviderFactory = static () => typeof({{property.DeclaringType.FullyQualifiedName}}).GetMember({{FormatStringLiteral(property.MemberName)}}, {{InstanceMemberBindingFlagsVariableName}}) is { Length: > 0 } results ? results[0] : null, }; properties[{{i}}] = {{JsonMetadataServicesTypeRef}}.CreatePropertyInfo<{{propertyTypeFQN}}>({{OptionsLocalVariableName}}, {{InfoVarName}}{{i}}); @@ -711,7 +725,7 @@ private static void GenerateCtorParamMetadataInitFunc(SourceWriter writer, strin ParameterType = typeof({{spec.ParameterType.FullyQualifiedName}}), Position = {{spec.ParameterIndex}}, HasDefaultValue = {{FormatBoolLiteral(spec.HasDefaultValue)}}, - DefaultValue = {{CSharpSyntaxUtilities.FormatLiteral(spec.DefaultValue, spec.ParameterType)}}, + DefaultValue = {{(spec.HasDefaultValue ? CSharpSyntaxUtilities.FormatLiteral(spec.DefaultValue, spec.ParameterType) : "null")}}, IsNullable = {{FormatBoolLiteral(spec.IsNullable)}}, }, """); @@ -735,6 +749,7 @@ private static void GenerateCtorParamMetadataInitFunc(SourceWriter writer, strin Name = {{FormatStringLiteral(spec.Name)}}, ParameterType = typeof({{spec.ParameterType.FullyQualifiedName}}), Position = {{spec.ParameterIndex}}, + IsMemberInitializer = true, }, """); @@ -1099,7 +1114,14 @@ private static SourceText GetRootJsonContextImplementation(ContextGenerationSpec GetLogicForDefaultSerializerOptionsInit(contextSpec.GeneratedOptionsSpec, writer); - writer.WriteLine(); + writer.WriteLine($""" + + private const global::System.Reflection.BindingFlags {InstanceMemberBindingFlagsVariableName} = + global::System.Reflection.BindingFlags.Instance | + global::System.Reflection.BindingFlags.Public | + global::System.Reflection.BindingFlags.NonPublic; + + """); writer.WriteLine($$""" /// 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 f50564ba081fc..6ce961115720b 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -1263,6 +1263,7 @@ public static partial class JsonMetadataServices public sealed partial class JsonObjectInfoValues { public JsonObjectInfoValues() { } + public System.Func? ConstructorAttributeProviderFactory { get { throw null; } init { } } public System.Func? ConstructorParameterMetadataInitializer { get { throw null; } init { } } public System.Text.Json.Serialization.JsonNumberHandling NumberHandling { get { throw null; } init { } } public System.Func? ObjectCreator { get { throw null; } init { } } @@ -1270,12 +1271,26 @@ public JsonObjectInfoValues() { } public System.Func? PropertyMetadataInitializer { get { throw null; } init { } } public System.Action? SerializeHandler { get { throw null; } init { } } } + public abstract partial class JsonParameterInfo + { + internal JsonParameterInfo() { } + public System.Type DeclaringType { get { throw null; } } + public System.Reflection.ICustomAttributeProvider? AttributeProvider { get { throw null; } } + public object? DefaultValue { get { throw null; } } + public bool HasDefaultValue { get { throw null; } } + public bool IsNullable { get { throw null; } } + public bool IsMemberInitializer { get { throw null; } } + public string Name { get { throw null; } } + public System.Type ParameterType { get { throw null; } } + public int Position { get { throw null; } } + } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public sealed partial class JsonParameterInfoValues { public JsonParameterInfoValues() { } public object? DefaultValue { get { throw null; } init { } } public bool HasDefaultValue { get { throw null; } init { } } + public bool IsMemberInitializer { get { throw null; } init { } } public bool IsNullable { get { throw null; } init { } } public string Name { get { throw null; } init { } } public System.Type ParameterType { get { throw null; } init { } } @@ -1293,8 +1308,10 @@ public JsonPolymorphismOptions() { } public abstract partial class JsonPropertyInfo { internal JsonPropertyInfo() { } + public System.Text.Json.Serialization.Metadata.JsonParameterInfo? AssociatedParameter { get { throw null; } } public System.Reflection.ICustomAttributeProvider? AttributeProvider { get { throw null; } set { } } public System.Text.Json.Serialization.JsonConverter? CustomConverter { get { throw null; } set { } } + public System.Type DeclaringType { get { throw null; } } public System.Func? Get { get { throw null; } set { } } public bool IsExtensionData { get { throw null; } set { } } public bool IsGetNullable { get { throw null; } set { } } @@ -1313,6 +1330,7 @@ internal JsonPropertyInfo() { } public sealed partial class JsonPropertyInfoValues { public JsonPropertyInfoValues() { } + public System.Func? AttributeProviderFactory { get; init; } public System.Text.Json.Serialization.JsonConverter? Converter { get { throw null; } init { } } public System.Type DeclaringType { get { throw null; } init { } } public System.Func? Getter { get { throw null; } init { } } @@ -1333,7 +1351,10 @@ public abstract partial class JsonTypeInfo internal JsonTypeInfo() { } public System.Text.Json.Serialization.JsonConverter Converter { get { throw null; } } public System.Func? CreateObject { get { throw null; } set { } } + public System.Reflection.ICustomAttributeProvider? ConstructorAttributeProvider { get { throw null; } } + public System.Type? ElementType { get { throw null; } } public bool IsReadOnly { get { throw null; } } + public System.Type? KeyType { get { throw null; } } 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 { } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/CastingConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/CastingConverter.cs index f607b62ef3a7f..fabf97436bd26 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/CastingConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/CastingConverter.cs @@ -15,6 +15,7 @@ internal sealed class CastingConverter : JsonConverter private readonly JsonConverter _sourceConverter; internal override Type? KeyType => _sourceConverter.KeyType; internal override Type? ElementType => _sourceConverter.ElementType; + internal override JsonConverter? NullableElementConverter => _sourceConverter.NullableElementConverter; public override bool HandleNull { get; } internal override bool SupportsCreateObjectDelegate => _sourceConverter.SupportsCreateObjectDelegate; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs index 8ac1b709b8b91..905da12217480 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs @@ -12,6 +12,7 @@ internal sealed class FSharpOptionConverter : JsonConverter typeof(TElement); + internal override JsonConverter? NullableElementConverter => _elementConverter; // 'None' is encoded using 'null' at runtime and serialized as 'null' in JSON. public override bool HandleNull => true; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpValueOptionConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpValueOptionConverter.cs index 8f63a705a6892..b74d89a01a1c8 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpValueOptionConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpValueOptionConverter.cs @@ -12,6 +12,7 @@ internal sealed class FSharpValueOptionConverter : JsonC where TValueOption : struct, IEquatable { internal override Type? ElementType => typeof(TElement); + internal override JsonConverter? NullableElementConverter => _elementConverter; // 'ValueNone' is encoded using 'default' at runtime and serialized as 'null' in JSON. public override bool HandleNull => true; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs index 88303c6284d1f..7808ca49443de 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs @@ -20,6 +20,7 @@ internal sealed class JsonMetadataServicesConverter : JsonResumableConverter< internal override Type? KeyType => Converter.KeyType; internal override Type? ElementType => Converter.ElementType; + internal override JsonConverter? NullableElementConverter => Converter.NullableElementConverter; public override bool HandleNull { get; } internal override bool ConstructorIsParameterized => Converter.ConstructorIsParameterized; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/JsonObjectConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/JsonObjectConverter.cs index 0a4bb183178db..3dd6f6319e744 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/JsonObjectConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/JsonObjectConverter.cs @@ -11,6 +11,5 @@ internal abstract class JsonObjectConverter : JsonResumableConverter { private protected sealed override ConverterStrategy GetDefaultConverterStrategy() => ConverterStrategy.Object; internal override bool CanPopulate => true; - internal sealed override Type? ElementType => null; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs index f435febc7f393..7a16b52e87ea7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs @@ -61,7 +61,7 @@ protected sealed override void InitializeConstructorArgumentCaches(ref ReadStack object?[] arguments = ArrayPool.Shared.Rent(typeInfo.ParameterCache.Count); foreach (JsonParameterInfo parameterInfo in typeInfo.ParameterCache) { - arguments[parameterInfo.Position] = parameterInfo.DefaultValue; + arguments[parameterInfo.Position] = parameterInfo.EffectiveDefaultValue; } state.Current.CtorArgumentState!.Arguments = arguments; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs index 0179dc68310fc..9176c2847697b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs @@ -73,7 +73,7 @@ private static bool TryRead( if (info.IgnoreNullTokensOnRead) { // Use default value specified on parameter, if any. - value = info.DefaultValue; + value = info.EffectiveDefaultValue; } else if (!info.IsNullable && info.Options.RespectNullableAnnotations) { @@ -102,16 +102,16 @@ protected override void InitializeConstructorArgumentCaches(ref ReadStack state, switch (parameterInfo.Position) { case 0: - arguments.Arg0 = ((JsonParameterInfo)parameterInfo).DefaultValue; + arguments.Arg0 = ((JsonParameterInfo)parameterInfo).EffectiveDefaultValue; break; case 1: - arguments.Arg1 = ((JsonParameterInfo)parameterInfo).DefaultValue; + arguments.Arg1 = ((JsonParameterInfo)parameterInfo).EffectiveDefaultValue; break; case 2: - arguments.Arg2 = ((JsonParameterInfo)parameterInfo).DefaultValue; + arguments.Arg2 = ((JsonParameterInfo)parameterInfo).EffectiveDefaultValue; break; case 3: - arguments.Arg3 = ((JsonParameterInfo)parameterInfo).DefaultValue; + arguments.Arg3 = ((JsonParameterInfo)parameterInfo).EffectiveDefaultValue; break; default: Debug.Fail("More than 4 params: we should be in override for LargeObjectWithParameterizedConstructorConverter."); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs index 3d6ca980e11c0..34cf9809cc087 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs @@ -610,7 +610,7 @@ protected static bool TryLookupConstructorParameter( // For case insensitive and missing property support of JsonPath, remember the value on the temporary stack. state.Current.JsonPropertyName = utf8PropertyName; - jsonParameterInfo = jsonPropertyInfo.ParameterInfo; + jsonParameterInfo = jsonPropertyInfo.AssociatedParameter; if (jsonParameterInfo != null) { state.Current.JsonPropertyInfo = null; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs index 7bf8b73359a5e..d5803e65b2cc8 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs @@ -8,6 +8,7 @@ namespace System.Text.Json.Serialization.Converters internal sealed class NullableConverter : JsonConverter where T : struct { internal override Type? ElementType => typeof(T); + internal override JsonConverter? NullableElementConverter => _elementConverter; public override bool HandleNull => true; internal override bool CanPopulate => _elementConverter.CanPopulate; internal override bool ConstructorIsParameterized => _elementConverter.ConstructorIsParameterized; @@ -21,7 +22,6 @@ public NullableConverter(JsonConverter elementConverter) _elementConverter = elementConverter; IsInternalConverterForNumberType = elementConverter.IsInternalConverterForNumberType; ConverterStrategy = elementConverter.ConverterStrategy; - ConstructorInfo = elementConverter.ConstructorInfo; } internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, scoped ref ReadStack state, out T? value) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs index fdeded799981c..0d62bcc795375 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs @@ -152,9 +152,11 @@ internal JsonConverter CreateCastingConverter() /// internal virtual JsonConverter? SourceConverterForCastingConverter => null; - internal abstract Type? ElementType { get; } + internal virtual Type? ElementType => null; - internal abstract Type? KeyType { get; } + internal virtual Type? KeyType => null; + + internal virtual JsonConverter? NullableElementConverter => null; /// /// Cached value of TypeToConvert.IsValueType, which is an expensive call. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs index 7537fa0d833be..4ab1eca5b1a5d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs @@ -32,10 +32,6 @@ protected JsonConverterFactory() { } /// public abstract JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options); - internal sealed override Type? KeyType => null; - - internal sealed override Type? ElementType => null; - internal JsonConverter GetConverterInternal(Type typeToConvert, JsonSerializerOptions options) { Debug.Assert(CanConvert(typeToConvert)); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs index cf8b87696ea71..cce660169ac28 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs @@ -57,10 +57,6 @@ internal sealed override JsonTypeInfo CreateJsonTypeInfo(JsonSerializerOptions o return new JsonTypeInfo(this, options); } - internal override Type? KeyType => null; - - internal override Type? ElementType => null; - /// /// Indicates whether should be passed to the converter on serialization, /// and whether should be passed on deserialization. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs index c5d44694c7193..05ae6cec8721a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs @@ -72,7 +72,7 @@ private static JsonTypeInfo CreateTypeInfoCore(Type type, JsonConverter converte typeInfo.SetCreateObjectIfCompatible(createObject); typeInfo.CreateObjectForExtensionDataProperty = createObject; - if (typeInfo.Kind is JsonTypeInfoKind.Object) + if (typeInfo is { Kind: JsonTypeInfoKind.Object, IsNullable: false }) { // If the System.Reflection.NullabilityInfoContext.IsSupported feature switch has been disabled, // we want to avoid resolving nullability information for properties and parameters unless the @@ -87,6 +87,8 @@ private static JsonTypeInfo CreateTypeInfoCore(Type type, JsonConverter converte } PopulateProperties(typeInfo, nullabilityCtx); + + typeInfo.ConstructorAttributeProvider = typeInfo.Converter.ConstructorInfo; } // Plug in any converter configuration -- should be run last. @@ -138,26 +140,6 @@ private static void PopulateProperties(JsonTypeInfo typeInfo, NullabilityInfoCon BindingFlags.NonPublic | BindingFlags.DeclaredOnly; - /// - /// Looks up the type for a member matching the given name and member type. - /// - [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] - internal static MemberInfo? LookupMemberInfo(Type type, MemberTypes memberType, string name) - { - Debug.Assert(memberType is MemberTypes.Field or MemberTypes.Property); - - // Walk the type hierarchy starting from the current type up to the base type(s) - foreach (Type t in type.GetSortedTypeHierarchy()) - { - MemberInfo[] members = t.GetMember(name, memberType, AllInstanceMembers); - if (members.Length > 0) - { - return members[0]; - } - } - - return null; - } [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs index b9cf70297f1ab..3b5648eddd024 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs @@ -53,6 +53,7 @@ private static JsonTypeInfo CreateCore(JsonSerializerOptions options, Json typeInfo.PropertyMetadataSerializationNotSupported = true; } + typeInfo.ConstructorAttributeProviderFactory = objectInfo.ConstructorAttributeProviderFactory; typeInfo.SerializeHandler = objectInfo.SerializeHandler; typeInfo.NumberHandling = objectInfo.NumberHandling; typeInfo.PopulatePolymorphismMetadata(); @@ -195,7 +196,7 @@ private static JsonPropertyInfo CreatePropertyInfoCore(JsonPropertyInfoVal propertyInfo.IgnoreCondition = propertyInfoValues.IgnoreCondition; propertyInfo.JsonTypeInfo = propertyInfoValues.PropertyTypeInfo; propertyInfo.NumberHandling = propertyInfoValues.NumberHandling; - propertyInfo.IsSourceGenerated = true; + propertyInfo.AttributeProviderFactory = propertyInfoValues.AttributeProviderFactory; return propertyInfo; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonObjectInfoValuesOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonObjectInfoValuesOfT.cs index 29022e6947dcd..5e704a2be7e05 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonObjectInfoValuesOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonObjectInfoValuesOfT.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; +using System.Reflection; namespace System.Text.Json.Serialization.Metadata { @@ -37,6 +38,11 @@ public sealed class JsonObjectInfoValues /// This API is for use by the output of the System.Text.Json source generator and should not be called directly. public Func? ConstructorParameterMetadataInitializer { get; init; } + /// + /// Provides a delayed attribute provider corresponding to the deserialization constructor. + /// + public Func? ConstructorAttributeProviderFactory { get; init; } + /// /// Specifies how number properties and fields should be processed when serializing and deserializing. /// 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 3f898a21b9a3a..6adb7a7d8fed3 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 @@ -2,14 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; namespace System.Text.Json.Serialization.Metadata { /// - /// Holds relevant state about a method parameter, like the default value of - /// the parameter, and the position in the method's parameter list. + /// Provides JSON serialization-related metadata about a constructor parameter. /// - internal abstract class JsonParameterInfo + public abstract class JsonParameterInfo { internal JsonParameterInfo(JsonParameterInfoValues parameterInfoValues, JsonPropertyInfo matchingProperty) { @@ -18,29 +19,109 @@ internal JsonParameterInfo(JsonParameterInfoValues parameterInfoValues, JsonProp Position = parameterInfoValues.Position; Name = parameterInfoValues.Name; HasDefaultValue = parameterInfoValues.HasDefaultValue; - IsNullable = parameterInfoValues.IsNullable; + DefaultValue = parameterInfoValues.HasDefaultValue ? parameterInfoValues.DefaultValue : null; MatchingProperty = matchingProperty; + IsMemberInitializer = parameterInfoValues.IsMemberInitializer; } + /// + /// Gets the declaring type of the constructor. + /// + public Type DeclaringType => MatchingProperty.DeclaringType; + + /// + /// Gets the zero-based position of the parameter in the formal parameter list. + /// public int Position { get; } + + /// + /// Gets the type of this parameter. + /// + public Type ParameterType => MatchingProperty.PropertyType; + + /// + /// Gets the name of the parameter. + /// public string Name { get; } + + /// + /// Gets a value indicating whether the parameter has a default value. + /// public bool HasDefaultValue { get; } - // The default value of the parameter. This is `DefaultValue` of the `ParameterInfo`, if specified, or the `default` for the `ParameterType`. - public object? DefaultValue { get; private protected init; } - public JsonPropertyInfo MatchingProperty { get; } - public bool IsNullable { get; internal set; } + /// + /// Gets a value indicating the default value if the parameter has a default value. + /// + public object? DefaultValue { get; } - public Type DeclaringType => MatchingProperty.DeclaringType; - public Type ParameterType => MatchingProperty.PropertyType; - public JsonConverter EffectiveConverter => MatchingProperty.EffectiveConverter; - public bool IgnoreNullTokensOnRead => MatchingProperty.IgnoreNullTokensOnRead; - public JsonSerializerOptions Options => MatchingProperty.Options; + /// + /// The default value to be passed to the constructor argument array, replacing null with default(TParameter). + /// + internal object? EffectiveDefaultValue { get; private protected init; } + + /// + /// Gets a value indicating whether the constructor parameter is annotated as nullable. + /// + /// + /// Contracts originating from or , + /// derive the value of this parameter from nullable reference type annotations, including annotations + /// from attributes such as or . + /// + /// This property has no effect on deserialization unless the + /// property has been enabled, in which case the serializer will reject any deserialization results. + /// + /// This setting is in sync with the associated property. + /// + public bool IsNullable => MatchingProperty.IsSetNullable; + + /// + /// Gets a value indicating whether the parameter represents a required or init-only member initializer. + /// + /// + /// Only returns for source generated metadata which can only access + /// required or init-only member initializers using object initialize expressions. + /// + public bool IsMemberInitializer { get; } + + /// + /// Gets a custom attribute provider for the current parameter. + /// + /// + /// When resolving metadata via the built-in resolvers this will be populated with + /// the underlying of the constructor metadata. + /// + public ICustomAttributeProvider? AttributeProvider + { + get + { + // Use delayed initialization to ensure that reflection dependencies are pay-for-play. + Debug.Assert(MatchingProperty.DeclaringTypeInfo != null, "Declaring type metadata must have already been configured."); + ICustomAttributeProvider? parameterInfo = _attributeProvider; + if (parameterInfo is null && MatchingProperty.DeclaringTypeInfo.ConstructorAttributeProvider is MethodBase ctorInfo) + { + ParameterInfo[] parameters = ctorInfo.GetParameters(); + if (Position < parameters.Length) + { + _attributeProvider = parameterInfo = parameters[Position]; + } + } + + return parameterInfo; + } + } + + private ICustomAttributeProvider? _attributeProvider; + + internal JsonPropertyInfo MatchingProperty { get; } + + internal JsonConverter EffectiveConverter => MatchingProperty.EffectiveConverter; + internal bool IgnoreNullTokensOnRead => MatchingProperty.IgnoreNullTokensOnRead; + internal JsonSerializerOptions Options => MatchingProperty.Options; // The effective name of the parameter as UTF-8 bytes. - public byte[] JsonNameAsUtf8Bytes => MatchingProperty.NameAsUtf8Bytes; - public JsonNumberHandling? NumberHandling => MatchingProperty.EffectiveNumberHandling; - public JsonTypeInfo JsonTypeInfo => MatchingProperty.JsonTypeInfo; - public bool ShouldDeserialize => !MatchingProperty.IsIgnored; + internal byte[] JsonNameAsUtf8Bytes => MatchingProperty.NameAsUtf8Bytes; + internal JsonNumberHandling? NumberHandling => MatchingProperty.EffectiveNumberHandling; + internal JsonTypeInfo JsonTypeInfo => MatchingProperty.JsonTypeInfo; + internal bool ShouldDeserialize => !MatchingProperty.IsIgnored; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoOfT.cs index 273f86eb31666..342fddecd2270 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoOfT.cs @@ -13,19 +13,21 @@ internal sealed class JsonParameterInfo : JsonParameterInfo { public new JsonConverter EffectiveConverter => MatchingProperty.EffectiveConverter; public new JsonPropertyInfo MatchingProperty { get; } - public new T? DefaultValue { get; } + public new T? EffectiveDefaultValue { get; } public JsonParameterInfo(JsonParameterInfoValues parameterInfoValues, JsonPropertyInfo matchingPropertyInfo) : base(parameterInfoValues, matchingPropertyInfo) { Debug.Assert(parameterInfoValues.ParameterType == typeof(T)); + Debug.Assert(!matchingPropertyInfo.IsConfigured); - MatchingProperty = matchingPropertyInfo; - DefaultValue = parameterInfoValues.HasDefaultValue && parameterInfoValues.DefaultValue is not null - ? (T)parameterInfoValues.DefaultValue - : default; + if (parameterInfoValues is { HasDefaultValue: true, DefaultValue: object defaultValue }) + { + EffectiveDefaultValue = (T)defaultValue; + } - base.DefaultValue = DefaultValue; + MatchingProperty = matchingPropertyInfo; + base.EffectiveDefaultValue = EffectiveDefaultValue; } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoValues.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoValues.cs index 24ad156c6a60d..26d0b2199a389 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoValues.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoValues.cs @@ -47,5 +47,11 @@ public sealed class JsonParameterInfoValues /// /// This API is for use by the output of the System.Text.Json source generator and should not be called directly. public bool IsNullable { get; init; } + + /// + /// Whether the parameter represents a required or init-only member initializer. + /// + /// This API is for use by the output of the System.Text.Json source generator and should not be called directly. + public bool IsMemberInitializer { get; init; } } } 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 696a6dee1e7d5..6a805e80162bf 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 @@ -155,7 +155,7 @@ internal JsonIgnoreCondition? IgnoreCondition /// The instance has been locked for further modification. /// /// - /// When resolving metadata via this + /// When resolving metadata via the built-in resolvers this /// will be populated with the underlying of the serialized property or field. /// /// Setting a custom attribute provider will have no impact on the contract model, @@ -165,37 +165,30 @@ public ICustomAttributeProvider? AttributeProvider { get { - ICustomAttributeProvider attributeProvider = _attributeProvider ?? InitializeAttributeProvider(); - return ReferenceEquals(attributeProvider, s_nullAttributeProvider) ? null : attributeProvider; + Func? attributeProviderFactory = Volatile.Read(ref AttributeProviderFactory); + ICustomAttributeProvider? attributeProvider = _attributeProvider; + + if (attributeProvider is null && attributeProviderFactory is not null) + { + _attributeProvider = attributeProvider = attributeProviderFactory(); + Volatile.Write(ref AttributeProviderFactory, null); + } + + return attributeProvider; } set { VerifyMutable(); - _attributeProvider = value ?? s_nullAttributeProvider; + _attributeProvider = value; + Volatile.Write(ref AttributeProviderFactory, null); } } - // Because the getter can initialize its own backing field, we want to avoid races between the getter and setter. - // This is done using CAS on the single _attributeProvider field which employs the following encoding: - // null: not initialized, s_nullAttributeProvider: null, otherwise: _attributeProvider + // Metadata emanating from the source generator use delayed attribute provider initialization + // ensuring that reflection metadata resolution remains pay-for-play and is trimmable. + internal Func? AttributeProviderFactory; private ICustomAttributeProvider? _attributeProvider; - private static readonly ICustomAttributeProvider s_nullAttributeProvider = typeof(NullAttributeProviderPlaceholder); - private sealed class NullAttributeProviderPlaceholder; - - [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", - Justification = "Looks up members that are already being referenced by the source generator.")] - private ICustomAttributeProvider InitializeAttributeProvider() - { - // If the property is source generated, perform a reflection lookup of its MemberInfo. - // Avoids overhead of reflection at startup and makes this method trimmable if unused. - ICustomAttributeProvider? provider = IsSourceGenerated && MemberName != null - ? DefaultJsonTypeInfoResolver.LookupMemberInfo(DeclaringType, MemberType, MemberName) - : null; - - provider ??= s_nullAttributeProvider; - return Interlocked.CompareExchange(ref _attributeProvider, provider, null) ?? provider; - } /// /// Gets or sets a value indicating if the property or field should be replaced or populated during deserialization. @@ -231,7 +224,6 @@ public JsonObjectCreationHandling? ObjectCreationHandling internal string? MemberName { get; set; } internal MemberTypes MemberType { get; set; } internal bool IsVirtual { get; set; } - internal bool IsSourceGenerated { get; set; } /// /// Gets or sets a value indicating whether the return type of the getter is annotated as nullable. @@ -302,17 +294,11 @@ public bool IsSetNullable ThrowHelper.ThrowInvalidOperationException_PropertyTypeNotNullable(this); } - if (ParameterInfo != null) - { - // Ensure the new setting is reflected in the associated parameter. - ParameterInfo.IsNullable = value; - } - _isSetNullable = value; } } - private bool _isSetNullable; + private protected bool _isSetNullable; /// /// Specifies whether the current property is a special extension data property. @@ -372,7 +358,18 @@ public bool IsRequired private bool _isRequired; - internal JsonParameterInfo? ParameterInfo { get; private set; } + /// + /// Gets the constructor parameter associated with the current property. + /// + /// + /// Returns the metadata for the parameter in the + /// deserialization constructor that has been associated with the current property. + /// + /// A constructor parameter is matched to a property or field if they are of the + /// same type and have the same name, up to case insensitivity. Each constructor + /// parameter must be matched to exactly one property of field. + /// + public JsonParameterInfo? AssociatedParameter { get; internal set; } internal JsonPropertyInfo(Type declaringType, Type propertyType, JsonTypeInfo? declaringTypeInfo, JsonSerializerOptions options) { @@ -399,6 +396,11 @@ internal static JsonPropertyInfo GetPropertyPlaceholder() return info; } + /// + /// Gets the declaring type of the property. + /// + public Type DeclaringType { get; } + /// /// Gets the type of the current property. /// @@ -676,19 +678,6 @@ private void DetermineEffectiveObjectCreationHandlingForProperty() EffectiveObjectCreationHandling = effectiveObjectCreationHandling; } - private void DetermineParameterInfo() - { - Debug.Assert(DeclaringTypeInfo?.IsConfigured is false); - ParameterInfo = DeclaringTypeInfo.CreateMatchingParameterInfo(this); - if (ParameterInfo != null) - { - // Given that we have associated a constructor parameter to this property, - // deserialization is no longer governed by the property setter. - // Ensure nullability configuration is copied over from the parameter to the property. - _isSetNullable = ParameterInfo.IsNullable; - } - } - private bool NumberHandingIsApplicable() { if (EffectiveConverter.IsInternalConverterForNumberType) @@ -734,7 +723,7 @@ private bool NumberHandingIsApplicable() /// /// Creates a instance whose type matches that of the current property. /// - internal abstract JsonParameterInfo CreateJsonParameterInfo(JsonParameterInfoValues parameterInfoValues); + internal abstract void AddJsonParameterInfo(JsonParameterInfoValues parameterInfoValues); internal abstract bool GetMemberAndWriteJson(object obj, ref WriteStack state, Utf8JsonWriter writer); internal abstract bool GetMemberAndWriteJsonExtensionData(object obj, ref WriteStack state, Utf8JsonWriter writer); @@ -949,7 +938,7 @@ internal void EnsureChildOf(JsonTypeInfo parent) ThrowHelper.ThrowInvalidOperationException_JsonPropertyInfoIsBoundToDifferentJsonTypeInfo(this); } - DetermineParameterInfo(); + DeclaringTypeInfo.ResolveMatchingParameterInfo(this); } /// @@ -970,8 +959,6 @@ internal bool TryGetPrePopulatedValue(scoped ref ReadStack state) return value != null; } - internal Type DeclaringType { get; } - internal JsonTypeInfo JsonTypeInfo { get @@ -1054,7 +1041,7 @@ public JsonNumberHandling? NumberHandling /// /// Number handling after considering options and declaring type number handling /// - internal JsonNumberHandling? EffectiveNumberHandling { get; set; } + internal JsonNumberHandling? EffectiveNumberHandling { get; private set; } // Whether the property type can be null. internal abstract bool PropertyTypeCanBeNull { get; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs index ff38badacdc59..ff31cf97a353d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs @@ -112,8 +112,15 @@ private protected override void SetShouldSerialize(Delegate? predicate) internal override object? DefaultValue => default(T); internal override bool PropertyTypeCanBeNull => default(T) is null; - internal override JsonParameterInfo CreateJsonParameterInfo(JsonParameterInfoValues parameterInfoValues) - => new JsonParameterInfo(parameterInfoValues, this); + internal override void AddJsonParameterInfo(JsonParameterInfoValues parameterInfoValues) + { + Debug.Assert(!IsConfigured); + Debug.Assert(AssociatedParameter is null); + + AssociatedParameter = new JsonParameterInfo(parameterInfoValues, this); + // Overwrite the nullability annotation of property setter with the parameter. + _isSetNullable = parameterInfoValues.IsNullable; + } internal new JsonConverter EffectiveConverter { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoValuesOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoValuesOfT.cs index 624879e469051..78cb0e6ff902d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoValuesOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoValuesOfT.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; +using System.Reflection; namespace System.Text.Json.Serialization.Metadata { @@ -81,5 +82,10 @@ public sealed class JsonPropertyInfoValues /// The name to be used when processing the property or field, specified by . /// public string? JsonPropertyName { get; init; } + + /// + /// Provides a factory that maps to . + /// + public Func? AttributeProviderFactory { get; init; } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs index 297b9f0cb2dde..34860fdfefb8d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs @@ -27,7 +27,7 @@ public abstract partial class JsonTypeInfo // The number of parameters the deserialization constructor has. If this is not equal to ParameterCache.Count, this means // that not all parameters are bound to object properties, and an exception will be thrown if deserialization is attempted. - internal int ParameterCount { get; private set; } + internal int ParameterCount { get; private protected set; } // All of the serializable parameters on a POCO constructor keyed on parameter name. // Only parameters which bind to properties are cached. 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 9af88e14e81dd..41431ee40e3e1 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 @@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Pipelines; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; using System.Text.Json.Reflection; @@ -49,6 +50,27 @@ internal JsonTypeInfo(Type type, JsonConverter converter, JsonSerializerOptions KeyType = converter.KeyType; } + /// + /// Gets the element type corresponding to an enumerable, dictionary or optional type. + /// + /// + /// Returns the element type for enumerable types, the value type for dictionary types, + /// and the underlying type for or F# optional types. + /// + /// Returns for all other types or types using custom converters. + /// + public Type? ElementType { get; } + + /// + /// Gets the key type corresponding to a dictionary type. + /// + /// + /// Returns the key type for dictionary types. + /// + /// Returns for all other types or types using custom converters. + /// + public Type? KeyType { get; } + /// /// Gets or sets a parameterless factory to be used on deserialization. /// @@ -337,6 +359,8 @@ public JsonPolymorphismOptions? PolymorphismOptions // so it is allowed to be used for fast-path serialization but it will throw if used for metadata-based serialization internal bool PropertyMetadataSerializationNotSupported { get; set; } + internal bool IsNullable => Converter.NullableElementConverter is not null; + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void ValidateCanBeUsedForPropertyMetadataSerialization() { @@ -346,9 +370,6 @@ internal void ValidateCanBeUsedForPropertyMetadataSerialization() } } - internal Type? ElementType { get; } - internal Type? KeyType { get; } - /// /// Return the JsonTypeInfo for the element type, or null if the type is not an enumerable or dictionary. /// @@ -437,7 +458,7 @@ internal JsonTypeInfo? KeyTypeInfo /// User-defined custom converters (specified either via or ) /// are metadata-agnostic and thus always resolve to . /// - public JsonTypeInfoKind Kind { get; private set; } + public JsonTypeInfoKind Kind { get; } /// /// Dummy instance corresponding to the declaring type of this . @@ -599,6 +620,45 @@ public IJsonTypeInfoResolver? OriginatingResolver private IJsonTypeInfoResolver? _originatingResolver; + /// + /// Gets or sets an attribute provider corresponding to the deserialization constructor. + /// + /// + /// The instance has been locked for further modification. + /// + /// + /// When resolving metadata via the built-in resolvers this will be populated with + /// the underlying of the serialized property or field. + /// + public ICustomAttributeProvider? ConstructorAttributeProvider + { + get + { + Func? ctorAttrProviderFactory = Volatile.Read(ref ConstructorAttributeProviderFactory); + ICustomAttributeProvider? ctorAttrProvider = _constructorAttributeProvider; + + if (ctorAttrProvider is null && ctorAttrProviderFactory is not null) + { + _constructorAttributeProvider = ctorAttrProvider = ctorAttrProviderFactory(); + Volatile.Write(ref ConstructorAttributeProviderFactory, null); + } + + return ctorAttrProvider; + } + internal set + { + Debug.Assert(!IsReadOnly); + + _constructorAttributeProvider = value; + Volatile.Write(ref ConstructorAttributeProviderFactory, null); + } + } + + // Metadata emanating from the source generator use delayed attribute provider initialization + // ensuring that reflection metadata resolution remains pay-for-play and is trimmable. + internal Func? ConstructorAttributeProviderFactory; + private ICustomAttributeProvider? _constructorAttributeProvider; + internal void VerifyMutable() { if (IsReadOnly) @@ -986,7 +1046,7 @@ public JsonPropertyInfo CreateJsonPropertyInfo(Type propertyType, string name) return propertyInfo; } - private Dictionary? _parameterInfoValuesIndex; + private protected Dictionary? _parameterInfoValuesIndex; // Untyped, root-level serialization methods internal abstract void SerializeAsObject(Utf8JsonWriter writer, object? rootValue); @@ -1007,7 +1067,7 @@ internal ref struct PropertyHierarchyResolutionState(JsonSerializerOptions optio public bool IsPropertyOrderSpecified; } - private readonly struct ParameterLookupKey(Type type, string name) : IEquatable + private protected readonly struct ParameterLookupKey(Type type, string name) : IEquatable { public Type Type { get; } = type; public string Name { get; } = name; @@ -1105,25 +1165,23 @@ internal void PopulateParameterInfoValues(JsonParameterInfoValues[] parameterInf _parameterInfoValuesIndex = parameterIndex; } - internal JsonParameterInfo? CreateMatchingParameterInfo(JsonPropertyInfo propertyInfo) + internal void ResolveMatchingParameterInfo(JsonPropertyInfo propertyInfo) { Debug.Assert( - !Converter.ConstructorIsParameterized || _parameterInfoValuesIndex is not null, + CreateObjectWithArgs is null || _parameterInfoValuesIndex is not null, "Metadata with parameterized constructors must have populated parameter info metadata."); if (_parameterInfoValuesIndex is not { } index) { - return null; + return; } string propertyName = propertyInfo.MemberName ?? propertyInfo.Name; ParameterLookupKey propKey = new(propertyInfo.PropertyType, propertyName); if (index.TryGetValue(propKey, out JsonParameterInfoValues? matchingParameterInfoValues)) { - return propertyInfo.CreateJsonParameterInfo(matchingParameterInfoValues); + propertyInfo.AddJsonParameterInfo(matchingParameterInfoValues); } - - return null; } internal void ConfigureConstructorParameters() @@ -1139,7 +1197,7 @@ internal void ConfigureConstructorParameters() foreach (KeyValuePair kvp in PropertyCache.List) { JsonPropertyInfo propertyInfo = kvp.Value; - JsonParameterInfo? parameterInfo = propertyInfo.ParameterInfo; + JsonParameterInfo? parameterInfo = propertyInfo.AssociatedParameter; if (parameterInfo is null) { continue; @@ -1159,7 +1217,7 @@ internal void ConfigureConstructorParameters() parameterCache.Add(parameterInfo); } - if (ExtensionDataProperty is { ParameterInfo: not null }) + if (ExtensionDataProperty is { AssociatedParameter: not null }) { Debug.Assert(ExtensionDataProperty.MemberName != null, "Custom property info cannot be data extension property"); ThrowHelper.ThrowInvalidOperationException_ExtensionDataCannotBindToCtorParam(ExtensionDataProperty.MemberName, ExtensionDataProperty); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs index e52998775a7b5..7a00b678d5989 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs @@ -97,6 +97,22 @@ private protected override void SetCreateObject(Delegate? createObject) _createObject = untypedCreateObject; _typedCreateObject = typedCreateObject; + + // Clear any data related to the previously specified ctor + ConstructorAttributeProviderFactory = null; + ConstructorAttributeProvider = null; + + if (CreateObjectWithArgs is not null) + { + _parameterInfoValuesIndex = null; + CreateObjectWithArgs = null; + ParameterCount = 0; + + foreach (JsonPropertyInfo propertyInfo in PropertyList) + { + propertyInfo.AssociatedParameter = null; + } + } } /// diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.JsonSerializer.cs b/src/libraries/System.Text.Json/tests/Common/MetadataTests.JsonSerializer.cs similarity index 99% rename from src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.JsonSerializer.cs rename to src/libraries/System.Text.Json/tests/Common/MetadataTests.JsonSerializer.cs index d0c1cdeb21b5c..217241f21b565 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.JsonSerializer.cs +++ b/src/libraries/System.Text.Json/tests/Common/MetadataTests.JsonSerializer.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.IO; using System.Linq; using System.Text.Json.Serialization.Metadata; using System.Threading.Tasks; diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.Options.cs b/src/libraries/System.Text.Json/tests/Common/MetadataTests.Options.cs similarity index 99% rename from src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.Options.cs rename to src/libraries/System.Text.Json/tests/Common/MetadataTests.Options.cs index 924d518815099..8341415bd8481 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.Options.cs +++ b/src/libraries/System.Text.Json/tests/Common/MetadataTests.Options.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Reflection; -using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using Xunit; diff --git a/src/libraries/System.Text.Json/tests/Common/MetadataTests.cs b/src/libraries/System.Text.Json/tests/Common/MetadataTests.cs new file mode 100644 index 0000000000000..f25c5fe80a5be --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Common/MetadataTests.cs @@ -0,0 +1,530 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Text.Json.Serialization.Metadata; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public partial class MetadataTests(JsonSerializerWrapper serializerUnderTest) : SerializerTests(serializerUnderTest) + { + [Theory] + [InlineData(typeof(int))] + [InlineData(typeof(int?))] + [InlineData(typeof(string))] + [InlineData(typeof(List))] + [InlineData(typeof(Dictionary))] + [InlineData(typeof(ClassWithoutCtor))] + [InlineData(typeof(StructWithDefaultCtor?))] + [InlineData(typeof(IInterfaceWithProperties))] + [InlineData(typeof(IDerivedInterface))] + public void TypeWithoutConstructor_TypeInfoReportsNullCtorProvider(Type type) + { + JsonTypeInfo typeInfo = Serializer.GetTypeInfo(type); + + Assert.Null(typeInfo.ConstructorAttributeProvider); + } + + [Theory] + [InlineData(typeof(ClassWithDefaultCtor))] + [InlineData(typeof(StructWithDefaultCtor))] + [InlineData(typeof(ClassWithParameterizedCtor))] + [InlineData(typeof(ClassWithMultipleConstructors))] + [InlineData(typeof(DerivedClassWithShadowingProperties))] + public void TypeWithConstructor_TypeInfoReportsExpectedCtorProvider(Type typeWithCtor) + { + ConstructorInfo? expectedCtor = typeWithCtor.GetConstructors(BindingFlags.Public | BindingFlags.Instance) + .OrderByDescending(ctor => ctor.GetCustomAttribute() is not null) + .FirstOrDefault(); + + Assert.NotNull(expectedCtor); + + JsonTypeInfo typeInfo = Serializer.GetTypeInfo(typeWithCtor); + + Assert.Same(expectedCtor, typeInfo.ConstructorAttributeProvider); + } + + [Theory] + [InlineData(typeof(ClassWithDefaultCtor))] + [InlineData(typeof(StructWithDefaultCtor))] + [InlineData(typeof(ClassWithParameterizedCtor))] + [InlineData(typeof(ClassWithMultipleConstructors))] + [InlineData(typeof(DerivedClassWithShadowingProperties))] + public void TypeWithConstructor_SettingCtorDelegate_ResetsCtorAttributeProvider(Type typeWithCtor) + { + JsonTypeInfo typeInfo = Serializer.GetTypeInfo(typeWithCtor, mutable: true); + Assert.NotNull(typeInfo.ConstructorAttributeProvider); + + typeInfo.CreateObject = () => Activator.CreateInstance(typeWithCtor); + + Assert.Null(typeInfo.ConstructorAttributeProvider); + } + + [Theory] + [InlineData(typeof(ClassWithDefaultCtor))] + [InlineData(typeof(StructWithDefaultCtor))] + [InlineData(typeof(ClassWithParameterizedCtor))] + [InlineData(typeof(StructWithParameterizedCtor))] + [InlineData(typeof(ClassWithoutCtor))] + [InlineData(typeof(IInterfaceWithProperties))] + [InlineData(typeof(ClassWithMultipleConstructors))] + [InlineData(typeof(DerivedClassWithShadowingProperties))] + [InlineData(typeof(IDerivedInterface))] + public void JsonPropertyInfo_DeclaringType_HasExpectedValue(Type typeWithProperties) + { + JsonTypeInfo typeInfo = Serializer.GetTypeInfo(typeWithProperties); + Assert.NotEmpty(typeInfo.Properties); + + Assert.All(typeInfo.Properties, propertyInfo => + { + Assert.True(propertyInfo.DeclaringType.IsAssignableFrom(typeInfo.Type)); + }); + } + + [Theory] + [InlineData(typeof(ClassWithDefaultCtor))] + [InlineData(typeof(StructWithDefaultCtor))] + [InlineData(typeof(ClassWithParameterizedCtor))] + [InlineData(typeof(ClassWithoutCtor))] + [InlineData(typeof(StructWithParameterizedCtor))] + [InlineData(typeof(IInterfaceWithProperties))] + [InlineData(typeof(ClassWithMultipleConstructors))] + [InlineData(typeof(DerivedClassWithShadowingProperties))] + [InlineData(typeof(IDerivedInterface))] + public void JsonPropertyInfo_AttributeProvider_HasExpectedValue(Type typeWithProperties) + { + JsonTypeInfo typeInfo = Serializer.GetTypeInfo(typeWithProperties); + Assert.NotEmpty(typeInfo.Properties); + + Assert.All(typeInfo.Properties, propertyInfo => + { + MemberInfo? memberInfo = ResolveMember(typeWithProperties, propertyInfo.Name); + Assert.Same(memberInfo, propertyInfo.AttributeProvider); + }); + } + + [Theory] + [InlineData(typeof(ClassWithoutCtor))] + [InlineData(typeof(IInterfaceWithProperties))] + [InlineData(typeof(IDerivedInterface))] + public void TypeWithoutConstructor_JsonPropertyInfo_AssociatedParameter_IsNull(Type type) + { + JsonTypeInfo typeInfo = Serializer.GetTypeInfo(type); + Assert.NotEmpty(typeInfo.Properties); + + Assert.All(typeInfo.Properties, propertyInfo => Assert.Null(propertyInfo.AssociatedParameter)); + } + + + [Theory] + [InlineData(typeof(ClassWithDefaultCtor))] + [InlineData(typeof(StructWithDefaultCtor))] + [InlineData(typeof(ClassWithParameterizedCtor))] + [InlineData(typeof(StructWithParameterizedCtor))] + [InlineData(typeof(ClassWithMultipleConstructors))] + [InlineData(typeof(DerivedClassWithShadowingProperties))] + public void TypeWithConstructor_JsonPropertyInfo_AssociatedParameter_MatchesCtorParams(Type typeWithCtor) + { + ConstructorInfo? expectedCtor = typeWithCtor.GetConstructors(BindingFlags.Public | BindingFlags.Instance) + .OrderByDescending(ctor => ctor.GetCustomAttribute() is not null) + .FirstOrDefault(); + + Assert.NotNull(expectedCtor); + + Dictionary parameters = expectedCtor.GetParameters().ToDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase); + JsonTypeInfo typeInfo = Serializer.GetTypeInfo(typeWithCtor); + + Assert.All(typeInfo.Properties, jsonProperty => + { + JsonParameterInfo? jsonParameter = jsonProperty.AssociatedParameter; + + if (!parameters.TryGetValue(jsonProperty.Name, out ParameterInfo? parameter)) + { + Assert.Null(jsonParameter); + return; + } + + Assert.NotNull(jsonParameter); + + Assert.Equal(typeWithCtor, jsonParameter.DeclaringType); + Assert.Equal(parameter.Position, jsonParameter.Position); + Assert.Equal(parameter.ParameterType, jsonParameter.ParameterType); + Assert.Equal(parameter.Name, jsonParameter.Name); + Assert.Equal(parameter.Name, jsonProperty.Name, StringComparer.OrdinalIgnoreCase); + + Assert.Equal(parameter.HasDefaultValue, jsonParameter.HasDefaultValue); + Assert.Equal(GetDefaultValue(parameter), jsonParameter.DefaultValue); + Assert.Same(parameter, jsonParameter.AttributeProvider); + Assert.Equal(jsonProperty.IsSetNullable, jsonParameter.IsNullable); + Assert.False(jsonParameter.IsMemberInitializer); + + parameters.Remove(jsonProperty.Name); + }); + + Assert.Empty(parameters); + } + + [Theory] + [InlineData(typeof(ClassWithRequiredMember))] + [InlineData(typeof(ClassWithInitOnlyProperty))] + public void TypeWithRequiredOrInitMember_SourceGen_HasAssociatedParameterInfo(Type type) + { + JsonTypeInfo typeInfo = Serializer.GetTypeInfo(type); + JsonPropertyInfo propertyInfo = typeInfo.Properties.Single(); + + JsonParameterInfo? jsonParameter = propertyInfo.AssociatedParameter; + + if (Serializer.IsSourceGeneratedSerializer) + { + Assert.NotNull(jsonParameter); + + Assert.Equal(type, jsonParameter.DeclaringType); + Assert.Equal(0, jsonParameter.Position); + Assert.Equal(propertyInfo.PropertyType, jsonParameter.ParameterType); + Assert.Equal(propertyInfo.Name, jsonParameter.Name); + + Assert.False(jsonParameter.HasDefaultValue); + Assert.Null(jsonParameter.DefaultValue); + Assert.Null(jsonParameter.AttributeProvider); + Assert.Equal(propertyInfo.IsSetNullable, jsonParameter.IsNullable); + Assert.True(jsonParameter.IsMemberInitializer); + } + else + { + Assert.Null(jsonParameter); + } + } + + [Theory] + [InlineData(typeof(ClassWithDefaultCtor))] + [InlineData(typeof(StructWithDefaultCtor))] + [InlineData(typeof(ClassWithParameterizedCtor))] + [InlineData(typeof(StructWithParameterizedCtor))] + [InlineData(typeof(ClassWithRequiredMember))] + [InlineData(typeof(ClassWithInitOnlyProperty))] + [InlineData(typeof(ClassWithMultipleConstructors))] + [InlineData(typeof(DerivedClassWithShadowingProperties))] + public void TypeWithConstructor_SettingCtorDelegate_ResetsAssociatedParameters(Type typeWithCtor) + { + JsonTypeInfo typeInfo = Serializer.GetTypeInfo(typeWithCtor, mutable: true); + Assert.NotEmpty(typeInfo.Properties); + + typeInfo.CreateObject = () => Activator.CreateInstance(typeWithCtor); + + Assert.All(typeInfo.Properties, propertyInfo => Assert.Null(propertyInfo.AssociatedParameter)); + } + + [Theory] + [InlineData(typeof(int), null)] + [InlineData(typeof(int?), null)] + [InlineData(typeof(string), null)] + [InlineData(typeof(ClassWithDefaultCtor), null)] + [InlineData(typeof(StructWithDefaultCtor), null)] + [InlineData(typeof(StructWithDefaultCtor?), null)] + [InlineData(typeof(string[]), null)] + [InlineData(typeof(List), null)] + [InlineData(typeof(IList), null)] + [InlineData(typeof(ImmutableArray), null)] + [InlineData(typeof(DerivedList), null)] + [InlineData(typeof(DerivedListWithCustomConverter), null)] + [InlineData(typeof(Dictionary), typeof(Guid))] + [InlineData(typeof(IReadOnlyDictionary), typeof(Guid))] + [InlineData(typeof(ImmutableDictionary), typeof(Guid))] + [InlineData(typeof(DerivedDictionary), typeof(Guid))] + [InlineData(typeof(DerivedDictionaryWithCustomConverter), null)] + [InlineData(typeof(ArrayList), null)] + [InlineData(typeof(Hashtable), typeof(string))] + public void JsonTypeInfo_KeyType_ReturnsExpectedValue(Type type, Type? expectedKeyType) + { + JsonTypeInfo typeInfo = Serializer.GetTypeInfo(type); + Assert.Equal(expectedKeyType, typeInfo.KeyType); + } + + [Theory] + [InlineData(typeof(int), null)] + [InlineData(typeof(int?), typeof(int))] + [InlineData(typeof(string), null)] + [InlineData(typeof(ClassWithDefaultCtor), null)] + [InlineData(typeof(StructWithDefaultCtor), null)] + [InlineData(typeof(StructWithDefaultCtor?), typeof(StructWithDefaultCtor))] + [InlineData(typeof(string[]), typeof(string))] + [InlineData(typeof(List), typeof(string))] + [InlineData(typeof(IList), typeof(string))] + [InlineData(typeof(ImmutableArray), typeof(string))] + [InlineData(typeof(DerivedList), typeof((int, Guid)))] + [InlineData(typeof(DerivedListWithCustomConverter), null)] + [InlineData(typeof(Dictionary), typeof(int))] + [InlineData(typeof(IReadOnlyDictionary), typeof(int))] + [InlineData(typeof(ImmutableDictionary), typeof(int))] + [InlineData(typeof(DerivedDictionary), typeof(int))] + [InlineData(typeof(DerivedDictionaryWithCustomConverter), null)] + [InlineData(typeof(ArrayList), typeof(object))] + [InlineData(typeof(Hashtable), typeof(object))] + public void JsonTypeInfo_ElementType_ReturnsExpectedValue(Type type, Type? expectedKeyType) + { + JsonTypeInfo typeInfo = Serializer.GetTypeInfo(type); + Assert.Equal(expectedKeyType, typeInfo.ElementType); + } + + private static object? GetDefaultValue(ParameterInfo parameterInfo) + { + Type parameterType = parameterInfo.ParameterType; + object? defaultValue = parameterInfo.DefaultValue; + + if (defaultValue is null) + { + return null; + } + + // DBNull.Value is sometimes used as the default value (returned by reflection) of nullable params in place of null. + if (defaultValue == DBNull.Value && parameterType != typeof(DBNull)) + { + return null; + } + + // Default values of enums or nullable enums are represented using the underlying type and need to be cast explicitly + // cf. https://github.com/dotnet/runtime/issues/68647 + if (parameterType.IsEnum) + { + return Enum.ToObject(parameterType, defaultValue); + } + + if (Nullable.GetUnderlyingType(parameterType) is Type underlyingType && underlyingType.IsEnum) + { + return Enum.ToObject(underlyingType, defaultValue); + } + + return defaultValue; + } + + private static MemberInfo? ResolveMember(Type type, string name) + { + MemberInfo? result = type.GetMember(name, BindingFlags.Instance | BindingFlags.Public).FirstOrDefault(); + if (result is null && type.IsInterface) + { + return type.GetInterfaces().Select(i => ResolveMember(i, name)).FirstOrDefault(m => m is not null); + } + + return result; + } + + internal class ClassWithoutCtor + { + private ClassWithoutCtor() { } + + public int Value { get; set; } + public string Value2 { get; set; } + + [JsonInclude] + public bool Value3 = true; + } + + internal interface IInterfaceWithProperties + { + public int Value { get; set; } + public string Value2 { get; set; } + } + + internal struct StructWithDefaultCtor + { + [JsonConstructor] + public StructWithDefaultCtor() + { + } + + public int Value { get; set; } + public string Value2 { get; set; } + + [JsonInclude] + public bool Value3 = true; + } + + internal class ClassWithDefaultCtor + { + [JsonConstructor] + public ClassWithDefaultCtor() + { + } + + public int Value { get; set; } + public string Value2 { get; set; } + + [JsonInclude] + public bool Value3 = true; + } + + internal class ClassWithParameterizedCtor + { + [JsonConstructor] + public ClassWithParameterizedCtor( + string x1, int x2, bool x3, BindingFlags x4, + string x5 = "str", int x6 = 42, bool x7 = true, BindingFlags? x8 = BindingFlags.Instance) + { + X1 = x1; + X2 = x2; + X3 = x3; + X4 = x4; + X5 = x5; + X6 = x6; + X7 = x7; + X8 = x8; + } + + public string X1 { get; } + public int X2 { get; } + public bool X3 { get; } + public BindingFlags X4 { get; } + public string X5 { get; } + public int X6 { get; } + public bool X7 { get; } + + [JsonInclude] + public BindingFlags? X8; + + public string ExtraProperty { get; set; } + } + + internal struct StructWithParameterizedCtor + { + [JsonConstructor] + public StructWithParameterizedCtor( + string x1, int x2, bool x3, BindingFlags x4, + string x5 = "str", int x6 = 42, bool x7 = true, BindingFlags? x8 = BindingFlags.Instance) + { + X1 = x1; + X2 = x2; + X3 = x3; + X4 = x4; + X5 = x5; + X6 = x6; + X7 = x7; + X8 = x8; + } + + public string X1 { get; } + public int X2 { get; } + public bool X3 { get; } + public BindingFlags X4 { get; } + public string X5 { get; } + public int X6 { get; } + public bool X7 { get; } + + [JsonInclude] + public BindingFlags? X8; + + public string ExtraProperty { get; set; } + } + + internal class ClassWithRequiredMember + { + public required int Value { get; set; } + } + + internal class ClassWithInitOnlyProperty + { + public int Value { get; init; } + } + + internal class ClassWithMultipleConstructors + { + public ClassWithMultipleConstructors() { } + + public ClassWithMultipleConstructors(int x) { } + + [JsonConstructor] + public ClassWithMultipleConstructors(string value) + { + Value = value; + } + + public string Value { get; set; } + } + + internal abstract class BaseClassWithProperties + { + public int Value1 { get; } + public virtual int Value2 { get; set; } + public abstract int Value3 { get; set; } + } + + internal class DerivedClassWithShadowingProperties : BaseClassWithProperties + { + [JsonConstructor] + public DerivedClassWithShadowingProperties(string value1, int value2, int value3) + { + Value1 = value1; + Value2 = value2; + Value3 = value3; + } + + public new string Value1 { get; set; } + public override int Value2 { get; set; } + public override int Value3 { get; set; } + } + + internal interface IBaseInterface1 + { + int Value1 { get; } + } + + internal interface IBaseInterface2 + { + int Value2 { get; set; } + } + + internal interface IDerivedInterface : IBaseInterface1, IBaseInterface2 + { + new string Value2 { get; set; } + int Value3 { get; set; } + } + + internal class DerivedList : List<(T, Guid)>; + + [JsonConverter(typeof(CustomConverter))] + internal class DerivedListWithCustomConverter : List + { + public sealed class CustomConverter : JsonConverter + { + public override DerivedListWithCustomConverter? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException(); + public override void Write(Utf8JsonWriter writer, DerivedListWithCustomConverter value, JsonSerializerOptions options) => throw new NotImplementedException(); + } + } + + internal class DerivedDictionary : Dictionary; + + [JsonConverter(typeof(CustomConverter))] + internal class DerivedDictionaryWithCustomConverter : Dictionary + { + public sealed class CustomConverter : JsonConverter + { + public override DerivedDictionaryWithCustomConverter? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException(); + public override void Write(Utf8JsonWriter writer, DerivedDictionaryWithCustomConverter value, JsonSerializerOptions options) => throw new NotImplementedException(); + } + } + } + + internal class WeatherForecastWithPOCOs + { + public DateTimeOffset Date { get; set; } + public int TemperatureCelsius { get; set; } + public string? Summary { get; set; } + public string? SummaryField; + public List? DatesAvailable { get; set; } + public Dictionary? TemperatureRanges { get; set; } + public string[]? SummaryWords { get; set; } + } + + public class HighLowTemps + { + public int High { get; set; } + public int Low { get; set; } + } + + [JsonSerializable(typeof(WeatherForecastWithPOCOs))] + internal sealed partial class JsonContext : JsonSerializerContext; +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/MetadataTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/MetadataTests.cs new file mode 100644 index 0000000000000..2dee7e6da78f4 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/MetadataTests.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Tests; + +namespace System.Text.Json.SourceGeneration.Tests +{ + public partial class MetadataTests_SourceGen() : MetadataTests(new StringSerializerWrapper(Context.Default)) + { + [JsonSerializable(typeof(int))] + [JsonSerializable(typeof(int?))] + [JsonSerializable(typeof(string))] + [JsonSerializable(typeof(string[]))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(IList))] + [JsonSerializable(typeof(ImmutableArray))] + [JsonSerializable(typeof(DerivedList))] + [JsonSerializable(typeof(DerivedListWithCustomConverter))] + [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(IReadOnlyDictionary))] + [JsonSerializable(typeof(ImmutableDictionary))] + [JsonSerializable(typeof(DerivedDictionary))] + [JsonSerializable(typeof(DerivedDictionaryWithCustomConverter))] + [JsonSerializable(typeof(ArrayList))] + [JsonSerializable(typeof(Hashtable))] + [JsonSerializable(typeof(ClassWithoutCtor))] + [JsonSerializable(typeof(IInterfaceWithProperties))] + [JsonSerializable(typeof(ClassWithDefaultCtor))] + [JsonSerializable(typeof(StructWithDefaultCtor))] + [JsonSerializable(typeof(StructWithDefaultCtor?))] + [JsonSerializable(typeof(ClassWithParameterizedCtor))] + [JsonSerializable(typeof(StructWithParameterizedCtor))] + [JsonSerializable(typeof(ClassWithRequiredMember))] + [JsonSerializable(typeof(ClassWithInitOnlyProperty))] + [JsonSerializable(typeof(ClassWithMultipleConstructors))] + [JsonSerializable(typeof(DerivedClassWithShadowingProperties))] + [JsonSerializable(typeof(IDerivedInterface))] + partial class Context : JsonSerializerContext; + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets index fdbc936afc1ac..7de497308e6ba 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets @@ -83,6 +83,9 @@ + + + @@ -121,6 +124,7 @@ + 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 eacb292bfc20f..631be65d8f24f 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 @@ -695,6 +695,7 @@ public static void CreateJsonPropertyInfo_ReturnsCorrectTypeOnPolymorphicConvert // The generic parameter of the property metadata type // should match that of property type and not the converter type. + Assert.Equal(typeof(MyClass), jpi.DeclaringType); Assert.Equal(typeof(string), jpi.GetType().GetGenericArguments()[0]); Assert.Equal(typeof(string), jpi.PropertyType); } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.cs index b1339b85cb746..a400e302ad635 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.cs @@ -1,86 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Generic; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Tests; - namespace System.Text.Json.Serialization.Tests { - public sealed class MetadataTests_Span : MetadataTests - { - public MetadataTests_Span() : base(JsonSerializerWrapper.SpanSerializer) { } - } - - public sealed class MetadataTests_String : MetadataTests - { - public MetadataTests_String() : base(JsonSerializerWrapper.StringSerializer) { } - } - - public sealed class MetadataTests_AsyncStream : MetadataTests - { - public MetadataTests_AsyncStream() : base(JsonSerializerWrapper.AsyncStreamSerializer) { } - } - - public sealed class MetadataTests_SyncStream : MetadataTests - { - public MetadataTests_SyncStream() : base(JsonSerializerWrapper.SyncStreamSerializer) { } - } - - public sealed class MetadataTests_LowLevel : MetadataTests - { - public MetadataTests_LowLevel() : base(JsonSerializerWrapper.ReaderWriterSerializer) { } - } - - public class MetadataTests_Document : MetadataTests - { - public MetadataTests_Document() : base(JsonSerializerWrapper.DocumentSerializer) { } - } - - public class MetadataTests_Element : MetadataTests - { - public MetadataTests_Element() : base(JsonSerializerWrapper.ElementSerializer) { } - } - - public class MetadataTests_Node : MetadataTests - { - public MetadataTests_Node() : base(JsonSerializerWrapper.NodeSerializer) { } - } - - public class MetadataTests_Pipe : MetadataTests - { - public MetadataTests_Pipe() : base(JsonSerializerWrapper.AsyncPipeSerializer) { } - } - - public abstract partial class MetadataTests - { - protected JsonSerializerWrapper Serializer { get; } - - public MetadataTests(JsonSerializerWrapper serializer) - { - Serializer = serializer; - } - } - - internal class WeatherForecastWithPOCOs - { - public DateTimeOffset Date { get; set; } - public int TemperatureCelsius { get; set; } - public string? Summary { get; set; } - public string? SummaryField; - public List? DatesAvailable { get; set; } - public Dictionary? TemperatureRanges { get; set; } - public string[]? SummaryWords { get; set; } - } - - public class HighLowTemps - { - public int High { get; set; } - public int Low { get; set; } - } - - [JsonSerializable(typeof(WeatherForecastWithPOCOs))] - internal sealed partial class JsonContext : JsonSerializerContext - { - } + public sealed class MetadataTests_String() : MetadataTests(JsonSerializerWrapper.StringSerializer); + public sealed class MetadataTests_AsyncStream() : MetadataTests(JsonSerializerWrapper.AsyncStreamSerializer); } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj index bbddf00081650..72a7a32210a31 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj @@ -68,6 +68,9 @@ + + + @@ -195,8 +198,6 @@ - -