Fix a couple of JsonConstructor bugs & clean up JsonParameterInfo implementation...
authorEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Thu, 9 Feb 2023 15:37:47 +0000 (15:37 +0000)
committerGitHub <noreply@github.com>
Thu, 9 Feb 2023 15:37:47 +0000 (15:37 +0000)
* Fix a couple of JsonConstructor bugs & clean up JsonParameterInfo implementation.

* add test coverage for ignored null values

* fix whitespace

18 files changed:
src/libraries/System.Text.Json/Common/ReflectionExtensions.cs
src/libraries/System.Text.Json/src/System.Text.Json.csproj
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Arguments.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultValueHolder.cs [deleted file]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoOfT.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs
src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs
src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Constructor.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs

index ed3414e..11fba3f 100644 (file)
@@ -319,14 +319,34 @@ namespace System.Text.Json.Reflection
 
         public static object? GetDefaultValue(this 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 && parameterInfo.ParameterType != typeof(DBNull))
+            if (defaultValue == DBNull.Value && parameterType != typeof(DBNull))
             {
                 return null;
             }
 
+#if !BUILDING_SOURCE_GENERATOR
+            // 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);
+            }
+#endif
+
             return defaultValue;
         }
 
index 16003dc..ea0c128 100644 (file)
@@ -243,7 +243,6 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
     <Compile Include="System\Text\Json\Serialization\JsonStringEnumConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonUnknownTypeHandling.cs" />
     <Compile Include="System\Text\Json\Serialization\Metadata\FSharpCoreReflectionProxy.cs" />
-    <Compile Include="System\Text\Json\Serialization\Metadata\DefaultValueHolder.cs" />
     <Compile Include="System\Text\Json\Serialization\Metadata\JsonCollectionInfoValuesOfTCollection.cs" />
     <Compile Include="System\Text\Json\Serialization\Metadata\JsonMetadataServices.Collections.cs" />
     <Compile Include="System\Text\Json\Serialization\Metadata\JsonMetadataServices.Converters.cs" />
index 2b33133..ddf6972 100644 (file)
@@ -9,9 +9,9 @@ namespace System.Text.Json
     /// </summary>
     internal sealed class Arguments<TArg0, TArg1, TArg2, TArg3>
     {
-        public TArg0 Arg0 = default!;
-        public TArg1 Arg1 = default!;
-        public TArg2 Arg2 = default!;
-        public TArg3 Arg3 = default!;
+        public TArg0? Arg0;
+        public TArg1? Arg1;
+        public TArg2? Arg2;
+        public TArg3? Arg3;
     }
 }
index c7ba3c9..e34aae0 100644 (file)
@@ -19,11 +19,11 @@ namespace System.Text.Json.Serialization.Converters
             Debug.Assert(jsonParameterInfo.ShouldDeserialize);
             Debug.Assert(jsonParameterInfo.Options != null);
 
-            bool success = jsonParameterInfo.ConverterBase.TryReadAsObject(ref reader, TypeToConvert, jsonParameterInfo.Options!, ref state, out object? arg);
+            bool success = jsonParameterInfo.EffectiveConverter.TryReadAsObject(ref reader, TypeToConvert, jsonParameterInfo.Options!, ref state, out object? arg);
 
             if (success && !(arg == null && jsonParameterInfo.IgnoreNullTokensOnRead))
             {
-                ((object[])state.Current.CtorArgumentState!.Arguments)[jsonParameterInfo.ClrInfo.Position] = arg!;
+                ((object[])state.Current.CtorArgumentState!.Arguments)[jsonParameterInfo.Position] = arg!;
 
                 // if this is required property IgnoreNullTokensOnRead will always be false because we don't allow for both to be true
                 state.Current.MarkRequiredPropertyAsRead(jsonParameterInfo.MatchingProperty);
@@ -60,7 +60,7 @@ namespace System.Text.Json.Serialization.Converters
             for (int i = 0; i < typeInfo.ParameterCount; i++)
             {
                 JsonParameterInfo parameterInfo = cache[i].Value;
-                arguments[parameterInfo.ClrInfo.Position] = parameterInfo.DefaultValue;
+                arguments[parameterInfo.Position] = parameterInfo.DefaultValue;
             }
 
             state.Current.CtorArgumentState!.Arguments = arguments;
index 5806092..2fb3f32 100644 (file)
@@ -31,7 +31,7 @@ namespace System.Text.Json.Serialization.Converters
 
             bool success;
 
-            switch (jsonParameterInfo.ClrInfo.Position)
+            switch (jsonParameterInfo.Position)
             {
                 case 0:
                     success = TryRead(ref state, ref reader, jsonParameterInfo, out arguments.Arg0);
@@ -57,19 +57,17 @@ namespace System.Text.Json.Serialization.Converters
             scoped ref ReadStack state,
             ref Utf8JsonReader reader,
             JsonParameterInfo jsonParameterInfo,
-            out TArg arg)
+            out TArg? arg)
         {
             Debug.Assert(jsonParameterInfo.ShouldDeserialize);
-            Debug.Assert(jsonParameterInfo.Options != null);
 
             var info = (JsonParameterInfo<TArg>)jsonParameterInfo;
-            var converter = (JsonConverter<TArg>)jsonParameterInfo.ConverterBase;
 
-            bool success = converter.TryRead(ref reader, info.PropertyType, info.Options!, ref state, out TArg? value);
+            bool success = info.EffectiveConverter.TryRead(ref reader, info.ParameterType, info.Options, ref state, out TArg? value);
 
-            arg = value == null && jsonParameterInfo.IgnoreNullTokensOnRead
-                ? (TArg?)info.DefaultValue! // Use default value specified on parameter, if any.
-                : value!;
+            arg = value is null && jsonParameterInfo.IgnoreNullTokensOnRead
+                ? info.DefaultValue // Use default value specified on parameter, if any.
+                : value;
 
             if (success)
             {
@@ -92,31 +90,23 @@ namespace System.Text.Json.Serialization.Converters
             for (int i = 0; i < typeInfo.ParameterCount; i++)
             {
                 JsonParameterInfo parameterInfo = cache[i].Value;
-
-                // We can afford not to set default values for ctor arguments when we should't deserialize because the
-                // type parameters of the `Arguments` type provide default semantics that work well with value types.
-                if (parameterInfo.ShouldDeserialize)
+                switch (parameterInfo.Position)
                 {
-                    int position = parameterInfo.ClrInfo.Position;
-
-                    switch (position)
-                    {
-                        case 0:
-                            arguments.Arg0 = ((JsonParameterInfo<TArg0>)parameterInfo).TypedDefaultValue!;
-                            break;
-                        case 1:
-                            arguments.Arg1 = ((JsonParameterInfo<TArg1>)parameterInfo).TypedDefaultValue!;
-                            break;
-                        case 2:
-                            arguments.Arg2 = ((JsonParameterInfo<TArg2>)parameterInfo).TypedDefaultValue!;
-                            break;
-                        case 3:
-                            arguments.Arg3 = ((JsonParameterInfo<TArg3>)parameterInfo).TypedDefaultValue!;
-                            break;
-                        default:
-                            Debug.Fail("More than 4 params: we should be in override for LargeObjectWithParameterizedConstructorConverter.");
-                            throw new InvalidOperationException();
-                    }
+                    case 0:
+                        arguments.Arg0 = ((JsonParameterInfo<TArg0>)parameterInfo).DefaultValue;
+                        break;
+                    case 1:
+                        arguments.Arg1 = ((JsonParameterInfo<TArg1>)parameterInfo).DefaultValue;
+                        break;
+                    case 2:
+                        arguments.Arg2 = ((JsonParameterInfo<TArg2>)parameterInfo).DefaultValue;
+                        break;
+                    case 3:
+                        arguments.Arg3 = ((JsonParameterInfo<TArg3>)parameterInfo).DefaultValue;
+                        break;
+                    default:
+                        Debug.Fail("More than 4 params: we should be in override for LargeObjectWithParameterizedConstructorConverter.");
+                        throw new InvalidOperationException();
                 }
             }
 
index 5a7f33e..a389a11 100644 (file)
@@ -445,7 +445,7 @@ namespace System.Text.Json.Serialization.Converters
                 // Returning false below will cause the read-ahead functionality to finish the read.
                 state.Current.PropertyState = StackFramePropertyState.ReadValue;
 
-                if (!SingleValueReadWithReadAhead(jsonParameterInfo.ConverterBase.RequiresReadAhead, ref reader, ref state))
+                if (!SingleValueReadWithReadAhead(jsonParameterInfo.EffectiveConverter.RequiresReadAhead, ref reader, ref state))
                 {
                     return false;
                 }
index a32063e..89bc7a2 100644 (file)
@@ -97,8 +97,6 @@ namespace System.Text.Json.Serialization
             throw new InvalidOperationException();
         }
 
-        internal abstract JsonParameterInfo CreateJsonParameterInfo();
-
         internal abstract JsonConverter<TTarget> CreateCastingConverter<TTarget>();
 
         /// <summary>
index 441cbf5..6d492a1 100644 (file)
@@ -32,13 +32,6 @@ namespace System.Text.Json.Serialization
         /// </returns>
         public abstract JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options);
 
-        internal override JsonParameterInfo CreateJsonParameterInfo()
-        {
-            Debug.Fail("We should never get here.");
-
-            throw new InvalidOperationException();
-        }
-
         internal sealed override Type? KeyType => null;
 
         internal sealed override Type? ElementType => null;
index ff811d8..d2a5a70 100644 (file)
@@ -56,11 +56,6 @@ namespace System.Text.Json.Serialization
             return new JsonTypeInfo<T>(this, options);
         }
 
-        internal sealed override JsonParameterInfo CreateJsonParameterInfo()
-        {
-            return new JsonParameterInfo<T>();
-        }
-
         internal sealed override JsonConverter<TTarget> CreateCastingConverter<TTarget>()
         {
             if (this is JsonConverter<TTarget> conv)
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultValueHolder.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultValueHolder.cs
deleted file mode 100644 (file)
index 224376d..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Diagnostics.CodeAnalysis;
-
-namespace System.Text.Json.Serialization.Metadata
-{
-    /// <summary>
-    /// Helper class used for calculating the default value for a given System.Type instance.
-    /// </summary>
-    internal sealed class DefaultValueHolder
-    {
-        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2067:UnrecognizedReflectionPattern",
-                    Justification = "GetUninitializedObject is only called on a struct. You can always create an instance of a struct.")]
-        private DefaultValueHolder(Type type)
-        {
-            if (type.IsValueType && Nullable.GetUnderlyingType(type) == null)
-            {
-#if NETCOREAPP
-                DefaultValue = System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(type);
-#else
-                DefaultValue = System.Runtime.Serialization.FormatterServices.GetUninitializedObject(type);
-#endif
-            }
-        }
-
-        /// <summary>
-        /// Returns the default value for the specified type.
-        /// </summary>
-        public object? DefaultValue { get; }
-
-        /// <summary>
-        /// Returns true if <param name="value"/> contains only default values.
-        /// </summary>
-        public bool IsDefaultValue(object value) => DefaultValue is null ? value is null : DefaultValue.Equals(value);
-
-        /// <summary>
-        /// Creates a holder instance representing a type.
-        /// </summary>
-        public static DefaultValueHolder CreateHolder(Type type) => new DefaultValueHolder(type);
-    }
-}
index 2f9ea58..5112d8b 100644 (file)
@@ -11,108 +11,43 @@ namespace System.Text.Json.Serialization.Metadata
     /// </summary>
     internal abstract class JsonParameterInfo
     {
-        private JsonTypeInfo? _jsonTypeInfo;
+        public JsonConverter EffectiveConverter => MatchingProperty.EffectiveConverter;
 
-        public JsonConverter ConverterBase { get; private set; } = null!;
+        // 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; }
 
-        private protected bool MatchingPropertyCanBeNull { get; private set; }
+        public bool IgnoreNullTokensOnRead { get; }
 
-        // The default value of the parameter. This is `DefaultValue` of the `ParameterInfo`, if specified, or the CLR `default` for the `ParameterType`.
-        public object? DefaultValue { get; private protected set; }
-
-        public bool IgnoreNullTokensOnRead { get; private set; }
-
-        // Options can be referenced here since all JsonPropertyInfos originate from a JsonTypeInfo that is cached on JsonSerializerOptions.
-        public JsonSerializerOptions? Options { get; set; } // initialized in Init method
+        public JsonSerializerOptions Options { get; }
 
         // The name of the parameter as UTF-8 bytes.
-        public byte[] NameAsUtf8Bytes { get; private set; } = null!;
+        public byte[] NameAsUtf8Bytes { get; }
 
-        public JsonNumberHandling? NumberHandling { get; private set; }
+        public JsonNumberHandling? NumberHandling { get; }
 
-        // Using a field to avoid copy semantics.
-        public JsonParameterInfoValues ClrInfo = null!;
+        public int Position { get; }
 
-        public JsonTypeInfo JsonTypeInfo
-        {
-            get
-            {
-                Debug.Assert(Options != null);
-                Debug.Assert(ShouldDeserialize);
-                return _jsonTypeInfo ??= Options.GetTypeInfoInternal(PropertyType);
-            }
-            set
-            {
-                // Used by JsonMetadataServices.
-                Debug.Assert(_jsonTypeInfo == null);
-                _jsonTypeInfo = value;
-            }
-        }
+        public JsonTypeInfo JsonTypeInfo => MatchingProperty.JsonTypeInfo;
 
-        public Type PropertyType { get; set; } = null!;
+        public Type ParameterType { get; }
 
-        public bool ShouldDeserialize { get; private set; }
+        public bool ShouldDeserialize { get; }
 
-        public JsonPropertyInfo MatchingProperty { get; private set; } = null!;
+        public JsonPropertyInfo MatchingProperty { get; }
 
-        public virtual void Initialize(JsonParameterInfoValues parameterInfo, JsonPropertyInfo matchingProperty, JsonSerializerOptions options)
+        public JsonParameterInfo(JsonParameterInfoValues parameterInfoValues, JsonPropertyInfo matchingProperty)
         {
+            Debug.Assert(matchingProperty.IsConfigured);
+
             MatchingProperty = matchingProperty;
-            ClrInfo = parameterInfo;
-            Options = options;
-            ShouldDeserialize = true;
+            ShouldDeserialize = !matchingProperty.IsIgnored;
+            Options = matchingProperty.Options;
+            Position = parameterInfoValues.Position;
 
-            PropertyType = matchingProperty.PropertyType;
-            NameAsUtf8Bytes = matchingProperty.NameAsUtf8Bytes!;
-            ConverterBase = matchingProperty.EffectiveConverter;
+            ParameterType = matchingProperty.PropertyType;
+            NameAsUtf8Bytes = matchingProperty.NameAsUtf8Bytes;
             IgnoreNullTokensOnRead = matchingProperty.IgnoreNullTokensOnRead;
             NumberHandling = matchingProperty.EffectiveNumberHandling;
-            MatchingPropertyCanBeNull = matchingProperty.PropertyTypeCanBeNull;
-        }
-
-        /// <summary>
-        /// Create a parameter that is ignored at run time. It uses the same type (typeof(sbyte)) to help
-        /// prevent issues with unsupported types and helps ensure we don't accidently (de)serialize it.
-        /// </summary>
-        public static JsonParameterInfo CreateIgnoredParameterPlaceholder(
-            JsonParameterInfoValues parameterInfo,
-            JsonPropertyInfo matchingProperty,
-            bool sourceGenMode)
-        {
-            JsonParameterInfo jsonParameterInfo = new JsonParameterInfo<sbyte>();
-            jsonParameterInfo.ClrInfo = parameterInfo;
-            jsonParameterInfo.PropertyType = matchingProperty.PropertyType;
-            jsonParameterInfo.NameAsUtf8Bytes = matchingProperty.NameAsUtf8Bytes!;
-
-            // TODO: https://github.com/dotnet/runtime/issues/60082.
-            // Default value initialization for params mapping to ignored properties doesn't
-            // account for the default value of optional parameters. This should be fixed.
-
-            if (sourceGenMode)
-            {
-                // The <T> value in the matching JsonPropertyInfo<T> instance matches the parameter type.
-                jsonParameterInfo.DefaultValue = matchingProperty.DefaultValue;
-            }
-            else
-            {
-                // The <T> value in the created JsonPropertyInfo<T> instance (sbyte)
-                // doesn't match the parameter type, use reflection to get the default value.
-                Type parameterType = parameterInfo.ParameterType;
-
-                DefaultValueHolder holder;
-                if (matchingProperty.Options.TryGetTypeInfoCached(parameterType, out JsonTypeInfo? typeInfo))
-                {
-                    holder = typeInfo.DefaultValueHolder;
-                }
-                else
-                {
-                    holder = DefaultValueHolder.CreateHolder(parameterInfo.ParameterType);
-                }
-
-                jsonParameterInfo.DefaultValue = holder.DefaultValue;
-            }
-
-            return jsonParameterInfo;
         }
     }
 }
index 180bd8c..5c1f57a 100644 (file)
@@ -11,36 +11,22 @@ namespace System.Text.Json.Serialization.Metadata
     /// </summary>
     internal sealed class JsonParameterInfo<T> : JsonParameterInfo
     {
-        public T TypedDefaultValue { get; private set; } = default!;
+        public new JsonConverter<T> EffectiveConverter => MatchingProperty.EffectiveConverter;
+        public new JsonPropertyInfo<T> MatchingProperty { get; }
+        public new T? DefaultValue { get; }
 
-        public override void Initialize(JsonParameterInfoValues parameterInfo, JsonPropertyInfo matchingProperty, JsonSerializerOptions options)
+        public JsonParameterInfo(JsonParameterInfoValues parameterInfoValues, JsonPropertyInfo<T> matchingPropertyInfo)
+            : base(parameterInfoValues, matchingPropertyInfo)
         {
-            base.Initialize(parameterInfo, matchingProperty, options);
-            InitializeDefaultValue(matchingProperty);
-        }
-
-        private void InitializeDefaultValue(JsonPropertyInfo matchingProperty)
-        {
-            Debug.Assert(ClrInfo.ParameterType == matchingProperty.PropertyType);
+            Debug.Assert(parameterInfoValues.ParameterType == typeof(T));
+            Debug.Assert(matchingPropertyInfo.IsConfigured);
 
-            if (ClrInfo.HasDefaultValue)
-            {
-                object? defaultValue = ClrInfo.DefaultValue;
+            MatchingProperty = matchingPropertyInfo;
+            DefaultValue = parameterInfoValues.HasDefaultValue && parameterInfoValues.DefaultValue is not null
+                ? (T)parameterInfoValues.DefaultValue
+                : default;
 
-                if (defaultValue == null && !matchingProperty.PropertyTypeCanBeNull)
-                {
-                    DefaultValue = TypedDefaultValue;
-                }
-                else
-                {
-                    DefaultValue = defaultValue;
-                    TypedDefaultValue = (T)defaultValue!;
-                }
-            }
-            else
-            {
-                DefaultValue = TypedDefaultValue;
-            }
+            base.DefaultValue = DefaultValue;
         }
     }
 }
index 90bfd38..220f489 100644 (file)
@@ -527,6 +527,11 @@ namespace System.Text.Json.Serialization.Metadata
                 || (shouldCheckForRequiredKeyword && memberInfo.HasRequiredMemberAttribute());
         }
 
+        /// <summary>
+        /// Creates a <see cref="JsonPropertyInfo"/> instance whose type matches that of the current property.
+        /// </summary>
+        internal abstract JsonParameterInfo CreateJsonParameterInfo(JsonParameterInfoValues parameterInfoValues);
+
         internal abstract bool GetMemberAndWriteJson(object obj, ref WriteStack state, Utf8JsonWriter writer);
         internal abstract bool GetMemberAndWriteJsonExtensionData(object obj, ref WriteStack state, Utf8JsonWriter writer);
 
index 772f589..f813f2c 100644 (file)
@@ -111,6 +111,8 @@ namespace System.Text.Json.Serialization.Metadata
 
         internal override object? DefaultValue => default(T);
         internal override bool PropertyTypeCanBeNull => default(T) is null;
+        internal override JsonParameterInfo CreateJsonParameterInfo(JsonParameterInfoValues parameterInfoValues)
+            => new JsonParameterInfo<T>(parameterInfoValues, this);
 
         internal new JsonConverter<T> EffectiveConverter
         {
index 123b5a1..e8d5e1b 100644 (file)
@@ -25,7 +25,7 @@ namespace System.Text.Json.Serialization.Metadata
 
         internal const string JsonObjectTypeName = "System.Text.Json.Nodes.JsonObject";
 
-        internal delegate T ParameterizedConstructorDelegate<T, TArg0, TArg1, TArg2, TArg3>(TArg0 arg0, TArg1 arg1, TArg2 arg2, TArg3 arg3);
+        internal delegate T ParameterizedConstructorDelegate<T, TArg0, TArg1, TArg2, TArg3>(TArg0? arg0, TArg1? arg1, TArg2? arg2, TArg3? arg3);
 
         /// <summary>
         /// Indices of required properties.
@@ -422,12 +422,6 @@ namespace System.Text.Json.Serialization.Metadata
         private protected abstract JsonPropertyInfo CreatePropertyInfoForTypeInfo();
 
         /// <summary>
-        /// Returns a helper class used for computing the default value.
-        /// </summary>
-        internal DefaultValueHolder DefaultValueHolder => _defaultValueHolder ??= DefaultValueHolder.CreateHolder(Type);
-        private DefaultValueHolder? _defaultValueHolder;
-
-        /// <summary>
         /// Gets or sets the type-level <see cref="JsonSerializerOptions.NumberHandling"/> override.
         /// </summary>
         /// <exception cref="InvalidOperationException">
@@ -1090,8 +1084,6 @@ namespace System.Text.Json.Serialization.Metadata
             Debug.Assert(ParameterCache is null);
 
             JsonParameterInfoValues[] jsonParameters = ParameterInfoValues ?? Array.Empty<JsonParameterInfoValues>();
-            bool sourceGenMode = Options.TypeInfoResolver is JsonSerializerContext;
-
             var parameterCache = new JsonPropertyDictionary<JsonParameterInfo>(Options.PropertyNameCaseInsensitive, jsonParameters.Length);
 
             // Cache the lookup from object property name to JsonPropertyInfo using a case-insensitive comparer.
@@ -1136,7 +1128,7 @@ namespace System.Text.Json.Serialization.Metadata
 
                     Debug.Assert(matchingEntry.JsonPropertyInfo != null);
                     JsonPropertyInfo jsonPropertyInfo = matchingEntry.JsonPropertyInfo;
-                    JsonParameterInfo jsonParameterInfo = CreateConstructorParameter(parameterInfo, jsonPropertyInfo, sourceGenMode, Options);
+                    JsonParameterInfo jsonParameterInfo = jsonPropertyInfo.CreateJsonParameterInfo(parameterInfo);
                     parameterCache.Add(jsonPropertyInfo.Name, jsonParameterInfo);
                 }
                 // It is invalid for the extension data property to bind to a constructor argument.
@@ -1256,25 +1248,6 @@ namespace System.Text.Json.Serialization.Metadata
             return new JsonPropertyDictionary<JsonPropertyInfo>(Options.PropertyNameCaseInsensitive, capacity);
         }
 
-        private static JsonParameterInfo CreateConstructorParameter(
-            JsonParameterInfoValues parameterInfo,
-            JsonPropertyInfo jsonPropertyInfo,
-            bool sourceGenMode,
-            JsonSerializerOptions options)
-        {
-            if (jsonPropertyInfo.IsIgnored)
-            {
-                return JsonParameterInfo.CreateIgnoredParameterPlaceholder(parameterInfo, jsonPropertyInfo, sourceGenMode);
-            }
-
-            JsonConverter converter = jsonPropertyInfo.EffectiveConverter;
-            JsonParameterInfo jsonParameterInfo = converter.CreateJsonParameterInfo();
-
-            jsonParameterInfo.Initialize(parameterInfo, jsonPropertyInfo, options);
-
-            return jsonParameterInfo;
-        }
-
         private static JsonTypeInfoKind GetTypeInfoKind(Type type, JsonConverter converter)
         {
             if (type == typeof(object) && converter.CanBePolymorphic)
index 8256b89..1199c7a 100644 (file)
@@ -361,6 +361,14 @@ namespace System.Text.Json.Serialization.Tests
         }
 
         [Fact]
+        public async Task IgnoreNullValues_SetDefaultConstructorParameter_ToConstructorArguments_ThatCanBeNull()
+        {
+            var options = new JsonSerializerOptions { IgnoreNullValues = true };
+            NullArgTester result = await Serializer.DeserializeWrapper<NullArgTester>(@"{""String"":null}", options);
+            Assert.Equal("defaultStr", result.String);
+        }
+
+        [Fact]
         public async Task NumerousSimpleAndComplexParameters()
         {
             var obj = await Serializer.DeserializeWrapper<ObjWCtorMixedParams>(ObjWCtorMixedParams.s_json);
@@ -1219,7 +1227,7 @@ namespace System.Text.Json.Serialization.Tests
         {
             string json = @"{""Prop"":20}";
             var obj1 = await Serializer.DeserializeWrapper<SmallType_IgnoredProp_Bind_ParamWithDefaultValue>(json);
-            Assert.Equal(0, obj1.Prop);
+            Assert.Equal(5, obj1.Prop);
 
             var obj2 = await Serializer.DeserializeWrapper<SmallType_IgnoredProp_Bind_Param>(json);
             Assert.Equal(0, obj2.Prop);
@@ -1248,7 +1256,7 @@ namespace System.Text.Json.Serialization.Tests
         {
             string json = @"{""Prop"":20}";
             var obj1 = await Serializer.DeserializeWrapper<LargeType_IgnoredProp_Bind_ParamWithDefaultValue>(json);
-            Assert.Equal(0, obj1.Prop);
+            Assert.Equal(5, obj1.Prop);
 
             var obj2 = await Serializer.DeserializeWrapper<LargeType_IgnoredProp_Bind_Param>(json);
             Assert.Equal(0, obj2.Prop);
@@ -1532,5 +1540,64 @@ namespace System.Text.Json.Serialization.Tests
                 ULong = @ulong;
             }
         }
+
+        [Fact]
+        public async Task TestTypeWithEnumParameters()
+        {
+            // Regression test for https://github.com/dotnet/runtime/issues/68647
+
+            var value = new TypeWithEnumParameters();
+
+            string json = await Serializer.SerializeWrapper(value);
+            Assert.Equal("""{"Value":"One","NullableValue":"Two"}""", json);
+
+            value = await Serializer.DeserializeWrapper<TypeWithEnumParameters>(json);
+            Assert.Equal(MyEnum.One, value.Value);
+            Assert.Equal(MyEnum.Two, value.NullableValue);
+
+            value = await Serializer.DeserializeWrapper<TypeWithEnumParameters>("{}");
+            Assert.Equal(MyEnum.One, value.Value);
+            Assert.Equal(MyEnum.Two, value.NullableValue);
+        }
+
+        public sealed class TypeWithEnumParameters
+        {
+            public MyEnum Value { get; }
+            public MyEnum? NullableValue { get; }
+
+            [JsonConstructor]
+            public TypeWithEnumParameters(MyEnum value = MyEnum.One, MyEnum? nullableValue = MyEnum.Two)
+            {
+                Value = value;
+                NullableValue = nullableValue;
+            }
+        }
+
+        [JsonConverter(typeof(JsonStringEnumConverter))]
+        public enum MyEnum
+        {
+            One = 1,
+            Two = 2,
+        }
+
+        [Fact]
+        public async Task TestClassWithIgnoredPropertyDefaultParam()
+        {
+            // Regression test for https://github.com/dotnet/runtime/issues/60082
+
+            string json = "{}";
+            ClassWithIgnoredPropertyDefaultParam result = await Serializer.DeserializeWrapper<ClassWithIgnoredPropertyDefaultParam>(json);
+            Assert.Equal(5, result.Y);
+        }
+
+        public class ClassWithIgnoredPropertyDefaultParam
+        {
+            public int X { get; }
+
+            [JsonIgnore]
+            public int Y { get; }
+
+            public ClassWithIgnoredPropertyDefaultParam(int x, int y = 5) => (X, Y) = (x, y);
+        }
     }
 }
index 28e9e2d..f4bf80d 100644 (file)
@@ -907,12 +907,14 @@ namespace System.Text.Json.Serialization.Tests
         public Point_3D_Struct Point3DStruct { get; }
         public ImmutableArray<int> ImmutableArray { get; }
         public int Int { get; }
+        public string String { get; }
 
-        public NullArgTester(Point_3D_Struct point3DStruct, ImmutableArray<int> immutableArray, int @int = 50)
+        public NullArgTester(Point_3D_Struct point3DStruct, ImmutableArray<int> immutableArray, int @int = 50, string @string = "defaultStr")
         {
             Point3DStruct = point3DStruct;
             ImmutableArray = immutableArray;
             Int = @int;
+            String = @string;
         }
     }
 
@@ -2057,7 +2059,7 @@ namespace System.Text.Json.Serialization.Tests
         public void Verify()
         {
             Assert.Equal(0, X);
-            Assert.Equal(0, Y); // We don't set parameter default value here.
+            Assert.Equal(5, Y); // We use the default parameter of the constructor.
         }
     }
     
index c4a5905..b41c9ed 100644 (file)
@@ -143,6 +143,8 @@ namespace System.Text.Json.SourceGeneration.Tests
         [JsonSerializable(typeof(ClassWithManyConstructorParameters))]
         [JsonSerializable(typeof(ClassWithInvalidArray))]
         [JsonSerializable(typeof(ClassWithInvalidDictionary))]
+        [JsonSerializable(typeof(TypeWithEnumParameters))]
+        [JsonSerializable(typeof(ClassWithIgnoredPropertyDefaultParam))]
         internal sealed partial class ConstructorTestsContext_Metadata : JsonSerializerContext
         {
         }
@@ -281,6 +283,8 @@ namespace System.Text.Json.SourceGeneration.Tests
         [JsonSerializable(typeof(ClassWithManyConstructorParameters))]
         [JsonSerializable(typeof(ClassWithInvalidArray))]
         [JsonSerializable(typeof(ClassWithInvalidDictionary))]
+        [JsonSerializable(typeof(TypeWithEnumParameters))]
+        [JsonSerializable(typeof(ClassWithIgnoredPropertyDefaultParam))]
         internal sealed partial class ConstructorTestsContext_Default : JsonSerializerContext
         {
         }