Repository: MessagePack-CSharp/MessagePack-CSharp

Issue:
Generated code expects data type to have default ctor despite having `[SerializationConstructor]` attribute
When migrating the vs-mef repo to v3, the `PropertyRef`, `MethodRef`, `FieldRef` and `DiscoveredParts` classes all have non-default constructors and the `[SerializationConstructor]` attribute on a non-default constructor. But the build fails because the source generated formatter tries to invoke a non-existent default constructor.

Attributed private constructors are apparently ignored, which explains the failures of the first 3 listed types.
AOT formatter should be more flexible for member/parameter types
The generated AOT formatter is invalid for the following class:

```cs
[SerializationConstructor]
public DiscoveredParts(IEnumerable<ComposablePartDefinition> parts, IEnumerable<PartDiscoveryException> discoveryErrors)
{
    this.Parts = ImmutableHashSet.CreateRange(parts);
    this.DiscoveryErrors = ImmutableList.CreateRange(discoveryErrors);
}

[Key(0)]
public ImmutableHashSet<ComposablePartDefinition> Parts { get; private set; }

[Key(1)]
public ImmutableList<PartDiscoveryException> DiscoveryErrors { get; private set; }
```

The formatter is correct when the parameter types match the member types, but it would be great if the formatter could deal with the parameter types being different from the member types, particularly when the member type is assignable to the parameter type (although the reverse would also be great to support, where the deserializer would just create the more derived type).
Generated code expects data type to have default ctor despite having `[SerializationConstructor]` attribute
When migrating the vs-mef repo to v3, the `PropertyRef`, `MethodRef`, `FieldRef` and `DiscoveredParts` classes all have non-default constructors and the `[SerializationConstructor]` attribute on a non-default constructor. But the build fails because the source generated formatter tries to invoke a non-existent default constructor.

Attributed private constructors are apparently ignored, which explains the failures of the first 3 listed types.
AOT formatter should be more flexible for member/parameter types
The generated AOT formatter is invalid for the following class:

```cs
[SerializationConstructor]
public DiscoveredParts(IEnumerable<ComposablePartDefinition> parts, IEnumerable<PartDiscoveryException> discoveryErrors)
{
    this.Parts = ImmutableHashSet.CreateRange(parts);
    this.DiscoveryErrors = ImmutableList.CreateRange(discoveryErrors);
}

[Key(0)]
public ImmutableHashSet<ComposablePartDefinition> Parts { get; private set; }

[Key(1)]
public ImmutableList<PartDiscoveryException> DiscoveryErrors { get; private set; }
```

The formatter is correct when the parameter types match the member types, but it would be great if the formatter could deal with the parameter types being different from the member types, particularly when the member type is assignable to the parameter type (although the reverse would also be great to support, where the deserializer would just create the more derived type).


Hints:

Patch:
diff --git a/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs b/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
index cc38d870d..587545fd1 100644
--- a/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
+++ b/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
@@ -9,6 +9,7 @@
 using Microsoft.CodeAnalysis;
 using Microsoft.CodeAnalysis.CSharp;
 using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Operations;
 
 namespace MessagePack.SourceGenerator.CodeAnalysis;
 
@@ -653,8 +654,8 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr
         }
 
         var isIntKey = true;
-        var intMembers = new Dictionary<int, MemberSerializationInfo>();
-        var stringMembers = new Dictionary<string, MemberSerializationInfo>();
+        var intMembers = new Dictionary<int, (MemberSerializationInfo Info, ITypeSymbol TypeSymbol)>();
+        var stringMembers = new Dictionary<string, (MemberSerializationInfo Info, ITypeSymbol TypeSymbol)>();
 
         FormatterDescriptor? GetSpecialFormatter(ISymbol member)
         {
@@ -705,7 +706,7 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr
                 includesPrivateMembers |= item.SetMethod is not null && !IsAllowedAccessibility(item.SetMethod.DeclaredAccessibility);
                 FormatterDescriptor? specialFormatter = GetSpecialFormatter(item);
                 var member = new MemberSerializationInfo(true, isWritable, isReadable, hiddenIntKey++, item.Name, item.Name, item.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Type.ToDisplayString(BinaryWriteFormat), specialFormatter);
-                stringMembers.Add(member.StringKey, member);
+                stringMembers.Add(member.StringKey, (member, item.Type));
 
                 if (specialFormatter is null)
                 {
@@ -725,7 +726,7 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr
 
                 FormatterDescriptor? specialFormatter = GetSpecialFormatter(item);
                 var member = new MemberSerializationInfo(false, IsWritable: !item.IsReadOnly, IsReadable: true, hiddenIntKey++, item.Name, item.Name, item.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Type.ToDisplayString(BinaryWriteFormat), specialFormatter);
-                stringMembers.Add(member.StringKey, member);
+                stringMembers.Add(member.StringKey, (member, item.Type));
                 if (specialFormatter is null)
                 {
                     this.CollectCore(item.Type); // recursive collect
@@ -827,7 +828,7 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr
                         }
 
                         var member = new MemberSerializationInfo(true, isWritable, isReadable, intKey!.Value, item.Name, item.Name, item.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Type.ToDisplayString(BinaryWriteFormat), specialFormatter);
-                        intMembers.Add(member.IntKey, member);
+                        intMembers.Add(member.IntKey, (member, item.Type));
                     }
                     else if (stringKey is not null)
                     {
@@ -837,7 +838,7 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr
                         }
 
                         var member = new MemberSerializationInfo(true, isWritable, isReadable, hiddenIntKey++, stringKey!, item.Name, item.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Type.ToDisplayString(BinaryWriteFormat), specialFormatter);
-                        stringMembers.Add(member.StringKey, member);
+                        stringMembers.Add(member.StringKey, (member, item.Type));
                     }
                 }
 
@@ -914,7 +915,7 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr
                         }
 
                         var member = new MemberSerializationInfo(true, IsWritable: !item.IsReadOnly, IsReadable: true, intKey!.Value, item.Name, item.Name, item.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Type.ToDisplayString(BinaryWriteFormat), specialFormatter);
-                        intMembers.Add(member.IntKey, member);
+                        intMembers.Add(member.IntKey, (member, item.Type));
                     }
                     else
                     {
@@ -924,7 +925,7 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr
                         }
 
                         var member = new MemberSerializationInfo(true, IsWritable: !item.IsReadOnly, IsReadable: true, hiddenIntKey++, stringKey!, item.Name, item.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Type.ToDisplayString(BinaryWriteFormat), specialFormatter);
-                        stringMembers.Add(member.StringKey, member);
+                        stringMembers.Add(member.StringKey, (member, item.Type));
                     }
                 }
 
@@ -934,10 +935,13 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr
 
         // GetConstructor
         var ctorEnumerator = default(IEnumerator<IMethodSymbol>);
-        var ctor = formattedType.Constructors.Where(x => IsAllowedAccessibility(x.DeclaredAccessibility)).SingleOrDefault(x => x.GetAttributes().Any(y => y.AttributeClass != null && y.AttributeClass.ApproximatelyEqual(this.typeReferences.SerializationConstructorAttribute)));
-        if (ctor == null)
+        var ctor = formattedType.Constructors.SingleOrDefault(x => x.GetAttributes().Any(y => y.AttributeClass != null && y.AttributeClass.ApproximatelyEqual(this.typeReferences.SerializationConstructorAttribute)));
+        if (ctor is null)
         {
-            ctorEnumerator = formattedType.Constructors.Where(x => IsAllowedAccessibility(x.DeclaredAccessibility)).OrderByDescending(x => x.Parameters.Length).GetEnumerator();
+            ctorEnumerator = formattedType.Constructors
+                .OrderByDescending(x => x.DeclaredAccessibility)
+                .ThenByDescending(x => x.Parameters.Length)
+                .GetEnumerator();
 
             if (ctorEnumerator.MoveNext())
             {
@@ -946,29 +950,31 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr
         }
 
         // struct allows null ctor
-        if (ctor == null && isClass)
+        if (ctor is null && isClass)
         {
             this.reportDiagnostic?.Invoke(Diagnostic.Create(MsgPack00xMessagePackAnalyzer.NoDeserializingConstructor, GetIdentifierLocation(formattedType)));
+            return null;
         }
 
         var constructorParameters = new List<MemberSerializationInfo>();
-        if (ctor != null)
+        if (ctor is not null)
         {
-            var constructorLookupDictionary = stringMembers.ToLookup(x => x.Value.Name, x => x, StringComparer.OrdinalIgnoreCase);
+            includesPrivateMembers |= !IsAllowedAccessibility(ctor.DeclaredAccessibility);
+
+            var constructorLookupDictionary = stringMembers.ToLookup(x => x.Value.Info.Name, x => x, StringComparer.OrdinalIgnoreCase);
             do
             {
                 constructorParameters.Clear();
                 var ctorParamIndex = 0;
                 foreach (IParameterSymbol item in ctor!.Parameters)
                 {
-                    MemberSerializationInfo paramMember;
                     if (isIntKey)
                     {
-                        if (intMembers.TryGetValue(ctorParamIndex, out paramMember!))
+                        if (intMembers.TryGetValue(ctorParamIndex, out (MemberSerializationInfo Info, ITypeSymbol TypeSymbol) member))
                         {
-                            if (item.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == paramMember.Type && paramMember.IsReadable)
+                            if (this.compilation.ClassifyConversion(member.TypeSymbol, item.Type) is { IsImplicit: true } && member.Info.IsReadable)
                             {
-                                constructorParameters.Add(paramMember);
+                                constructorParameters.Add(member.Info);
                             }
                             else
                             {
@@ -980,6 +986,7 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr
                                 else
                                 {
                                     this.reportDiagnostic?.Invoke(Diagnostic.Create(MsgPack00xMessagePackAnalyzer.DeserializingConstructorParameterTypeMismatch, GetLocation(item)));
+                                    return null;
                                 }
                             }
                         }
@@ -993,12 +1000,13 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr
                             else
                             {
                                 this.reportDiagnostic?.Invoke(Diagnostic.Create(MsgPack00xMessagePackAnalyzer.DeserializingConstructorParameterIndexMissing, GetParameterListLocation(ctor)));
+                                return null;
                             }
                         }
                     }
                     else
                     {
-                        IEnumerable<KeyValuePair<string, MemberSerializationInfo>> hasKey = constructorLookupDictionary[item.Name];
+                        IEnumerable<KeyValuePair<string, (MemberSerializationInfo Info, ITypeSymbol TypeSymbol)>> hasKey = constructorLookupDictionary[item.Name];
                         using var enumerator = hasKey.GetEnumerator();
 
                         // hasKey.Count() == 0
@@ -1007,6 +1015,7 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr
                             if (ctorEnumerator == null)
                             {
                                 this.reportDiagnostic?.Invoke(Diagnostic.Create(MsgPack00xMessagePackAnalyzer.DeserializingConstructorParameterNameMissing, GetParameterListLocation(ctor)));
+                                return null;
                             }
 
                             ctor = null;
@@ -1021,13 +1030,14 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr
                             if (ctorEnumerator == null)
                             {
                                 this.reportDiagnostic?.Invoke(Diagnostic.Create(MsgPack00xMessagePackAnalyzer.DeserializingConstructorParameterNameDuplicate, GetLocation(item)));
+                                return null;
                             }
 
                             ctor = null;
                             continue;
                         }
 
-                        paramMember = first;
+                        MemberSerializationInfo paramMember = first.Info;
                         if (item.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == paramMember.Type && paramMember.IsReadable)
                         {
                             constructorParameters.Add(paramMember);
@@ -1037,6 +1047,7 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr
                             if (ctorEnumerator == null)
                             {
                                 this.reportDiagnostic?.Invoke(Diagnostic.Create(MsgPack00xMessagePackAnalyzer.DeserializingConstructorParameterTypeMismatch, GetLocation(item)));
+                                return null;
                             }
 
                             ctor = null;
@@ -1102,7 +1113,7 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr
             genericTypeParameters: formattedType.IsGenericType ? formattedType.TypeParameters.Select(ToGenericTypeParameterInfo).ToArray() : Array.Empty<GenericTypeParameterInfo>(),
             constructorParameters: constructorParameters.ToArray(),
             isIntKey: isIntKey,
-            members: isIntKey ? intMembers.Values.ToArray() : stringMembers.Values.ToArray(),
+            members: isIntKey ? intMembers.Values.Select(v => v.Info).ToArray() : stringMembers.Values.Select(v => v.Info).ToArray(),
             hasIMessagePackSerializationCallbackReceiver: hasSerializationConstructor,
             needsCastOnAfter: needsCastOnAfter,
             needsCastOnBefore: needsCastOnBefore,


Test patch:
diff --git a/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs b/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs
index 9f17cc324..3ce8fdf53 100644
--- a/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs
+++ b/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs
@@ -3,6 +3,7 @@
 
 using MessagePack.SourceGenerator.Tests;
 using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Testing;
 using VerifyCS = CSharpSourceGeneratorVerifier<MessagePack.SourceGenerator.MessagePackGenerator>;
 
 public class GenerationTests
@@ -583,4 +584,70 @@ public void Serialize(ref MessagePackWriter writer, A value, MessagePackSerializ
         // This test is *not* expected to produce any generated files.
         await VerifyCS.Test.RunDefaultAsync(this.testOutputHelper, testSource);
     }
+
+    [Fact]
+    public async Task NonDefaultDeserializingConstructor_Private()
+    {
+        string testSource = """
+            using MessagePack;
+            [MessagePackObject]
+            partial class A {
+                [SerializationConstructor]
+                A(int x) => this.X = x;
+
+                [Key(0)]
+                internal int X { get; }
+            }
+            """;
+
+        await VerifyCS.Test.RunDefaultAsync(this.testOutputHelper, testSource);
+    }
+
+    [Fact]
+    public async Task DeserializingConstructorParameterMemberTypeAssignability_MemberAssignsToParamType()
+    {
+        string testSource = """
+            using MessagePack;
+            using System.Linq;
+            using System.Collections.Generic;
+            [MessagePackObject]
+            class A {
+                [SerializationConstructor]
+                internal A(IReadOnlyList<int> x) => this.X = x.ToList();
+
+                [Key(0)]
+                internal List<int> X { get; }
+            }
+            """;
+
+        await VerifyCS.Test.RunDefaultAsync(this.testOutputHelper, testSource);
+    }
+
+    [Fact]
+    public async Task DeserializingConstructorParameterMemberTypeAssignability_Incompatible()
+    {
+        string testSource = """
+            using MessagePack;
+            using System.Linq;
+            using System.Collections.Generic;
+            using System.Collections.Immutable;
+            [MessagePackObject]
+            class A {
+                [SerializationConstructor]
+                internal A({|#0:ImmutableList<int> x|}) => this.X = x.ToList();
+
+                [Key(0)]
+                internal List<int> X { get; }
+            }
+            """;
+
+        await new VerifyCS.Test
+        {
+            TestState =
+            {
+                Sources = { testSource },
+            },
+            ExpectedDiagnostics = { DiagnosticResult.CompilerError("MsgPack007").WithLocation(0) },
+        }.RunDefaultAsync(this.testOutputHelper);
+    }
 }
diff --git a/tests/MessagePack.SourceGenerator.Tests/Resources/DeserializingConstructorParameterMemberTypeAssignability_Incompatible/MessagePack.GeneratedMessagePackResolver.g.cs b/tests/MessagePack.SourceGenerator.Tests/Resources/DeserializingConstructorParameterMemberTypeAssignability_Incompatible/MessagePack.GeneratedMessagePackResolver.g.cs
new file mode 100644
index 000000000..363286fe6
--- /dev/null
+++ b/tests/MessagePack.SourceGenerator.Tests/Resources/DeserializingConstructorParameterMemberTypeAssignability_Incompatible/MessagePack.GeneratedMessagePackResolver.g.cs
@@ -0,0 +1,63 @@
+﻿// <auto-generated />
+
+#pragma warning disable 618, 612, 414, 168, CS1591, SA1129, SA1309, SA1312, SA1403, SA1649
+
+using MsgPack = global::MessagePack;
+
+[assembly: MsgPack::Internal.GeneratedAssemblyMessagePackResolverAttribute(typeof(MessagePack.GeneratedMessagePackResolver), 3, 0)]
+
+namespace MessagePack {
+
+/// <summary>A MessagePack resolver that uses generated formatters for types in this assembly.</summary>
+partial class GeneratedMessagePackResolver : MsgPack::IFormatterResolver
+{
+	/// <summary>An instance of this resolver that only returns formatters specifically generated for types in this assembly.</summary>
+	public static readonly MsgPack::IFormatterResolver Instance = new GeneratedMessagePackResolver();
+
+	private GeneratedMessagePackResolver()
+	{
+	}
+
+	public MsgPack::Formatters.IMessagePackFormatter<T> GetFormatter<T>()
+	{
+		return FormatterCache<T>.Formatter;
+	}
+
+	private static class FormatterCache<T>
+	{
+		internal static readonly MsgPack::Formatters.IMessagePackFormatter<T> Formatter;
+
+		static FormatterCache()
+		{
+			var f = GeneratedMessagePackResolverGetFormatterHelper.GetFormatter(typeof(T));
+			if (f != null)
+			{
+				Formatter = (MsgPack::Formatters.IMessagePackFormatter<T>)f;
+			}
+		}
+	}
+
+	private static class GeneratedMessagePackResolverGetFormatterHelper
+	{
+		private static readonly global::System.Collections.Generic.Dictionary<global::System.Type, int> closedTypeLookup = new(1)
+		{
+			{ typeof(global::System.Collections.Generic.List<int>), 0 },
+		};
+
+		internal static object GetFormatter(global::System.Type t)
+		{
+			if (closedTypeLookup.TryGetValue(t, out int closedKey))
+			{
+				return closedKey switch
+				{
+					0 => new MsgPack::Formatters.ListFormatter<int>(),
+					_ => null, // unreachable
+				};
+			}
+
+			return null;
+		}
+	}
+}
+
+}
diff --git a/tests/MessagePack.SourceGenerator.Tests/Resources/DeserializingConstructorParameterMemberTypeAssignability_MemberAssignsToParamType/Formatters.MessagePack.GeneratedMessagePackResolver.AFormatter.g.cs b/tests/MessagePack.SourceGenerator.Tests/Resources/DeserializingConstructorParameterMemberTypeAssignability_MemberAssignsToParamType/Formatters.MessagePack.GeneratedMessagePackResolver.AFormatter.g.cs
new file mode 100644
index 000000000..43f5630e2
--- /dev/null
+++ b/tests/MessagePack.SourceGenerator.Tests/Resources/DeserializingConstructorParameterMemberTypeAssignability_MemberAssignsToParamType/Formatters.MessagePack.GeneratedMessagePackResolver.AFormatter.g.cs
@@ -0,0 +1,59 @@
+﻿// <auto-generated />
+
+#pragma warning disable 618, 612, 414, 168, CS1591, SA1129, SA1309, SA1312, SA1403, SA1649
+
+#pragma warning disable CS8669 // We may leak nullable annotations into generated code.
+
+using MsgPack = global::MessagePack;
+
+namespace MessagePack {
+partial class GeneratedMessagePackResolver {
+
+	internal sealed class AFormatter : MsgPack::Formatters.IMessagePackFormatter<global::A>
+	{
+
+		public void Serialize(ref MsgPack::MessagePackWriter writer, global::A value, MsgPack::MessagePackSerializerOptions options)
+		{
+			if (value == null)
+			{
+				writer.WriteNil();
+				return;
+			}
+
+			MsgPack::IFormatterResolver formatterResolver = options.Resolver;
+			writer.WriteArrayHeader(1);
+			MsgPack::FormatterResolverExtensions.GetFormatterWithVerify<global::System.Collections.Generic.List<int>>(formatterResolver).Serialize(ref writer, value.X, options);
+		}
+
+		public global::A Deserialize(ref MsgPack::MessagePackReader reader, MsgPack::MessagePackSerializerOptions options)
+		{
+			if (reader.TryReadNil())
+			{
+				return null;
+			}
+
+			options.Security.DepthStep(ref reader);
+			MsgPack::IFormatterResolver formatterResolver = options.Resolver;
+			var length = reader.ReadArrayHeader();
+			var __X__ = default(global::System.Collections.Generic.List<int>);
+
+			for (int i = 0; i < length; i++)
+			{
+				switch (i)
+				{
+					case 0:
+						__X__ = MsgPack::FormatterResolverExtensions.GetFormatterWithVerify<global::System.Collections.Generic.List<int>>(formatterResolver).Deserialize(ref reader, options);
+						break;
+					default:
+						reader.Skip();
+						break;
+				}
+			}
+
+			var ____result = new global::A(__X__);
+			reader.Depth--;
+			return ____result;
+		}
+	}
+}
+}
diff --git a/tests/MessagePack.SourceGenerator.Tests/Resources/DeserializingConstructorParameterMemberTypeAssignability_MemberAssignsToParamType/MessagePack.GeneratedMessagePackResolver.g.cs b/tests/MessagePack.SourceGenerator.Tests/Resources/DeserializingConstructorParameterMemberTypeAssignability_MemberAssignsToParamType/MessagePack.GeneratedMessagePackResolver.g.cs
new file mode 100644
index 000000000..d07ffe633
--- /dev/null
+++ b/tests/MessagePack.SourceGenerator.Tests/Resources/DeserializingConstructorParameterMemberTypeAssignability_MemberAssignsToParamType/MessagePack.GeneratedMessagePackResolver.g.cs
@@ -0,0 +1,65 @@
+﻿// <auto-generated />
+
+#pragma warning disable 618, 612, 414, 168, CS1591, SA1129, SA1309, SA1312, SA1403, SA1649
+
+using MsgPack = global::MessagePack;
+
+[assembly: MsgPack::Internal.GeneratedAssemblyMessagePackResolverAttribute(typeof(MessagePack.GeneratedMessagePackResolver), 3, 0)]
+
+namespace MessagePack {
+
+/// <summary>A MessagePack resolver that uses generated formatters for types in this assembly.</summary>
+partial class GeneratedMessagePackResolver : MsgPack::IFormatterResolver
+{
+	/// <summary>An instance of this resolver that only returns formatters specifically generated for types in this assembly.</summary>
+	public static readonly MsgPack::IFormatterResolver Instance = new GeneratedMessagePackResolver();
+
+	private GeneratedMessagePackResolver()
+	{
+	}
+
+	public MsgPack::Formatters.IMessagePackFormatter<T> GetFormatter<T>()
+	{
+		return FormatterCache<T>.Formatter;
+	}
+
+	private static class FormatterCache<T>
+	{
+		internal static readonly MsgPack::Formatters.IMessagePackFormatter<T> Formatter;
+
+		static FormatterCache()
+		{
+			var f = GeneratedMessagePackResolverGetFormatterHelper.GetFormatter(typeof(T));
+			if (f != null)
+			{
+				Formatter = (MsgPack::Formatters.IMessagePackFormatter<T>)f;
+			}
+		}
+	}
+
+	private static class GeneratedMessagePackResolverGetFormatterHelper
+	{
+		private static readonly global::System.Collections.Generic.Dictionary<global::System.Type, int> closedTypeLookup = new(2)
+		{
+			{ typeof(global::System.Collections.Generic.List<int>), 0 },
+			{ typeof(global::A), 1 },
+		};
+
+		internal static object GetFormatter(global::System.Type t)
+		{
+			if (closedTypeLookup.TryGetValue(t, out int closedKey))
+			{
+				return closedKey switch
+				{
+					0 => new MsgPack::Formatters.ListFormatter<int>(),
+					1 => new global::MessagePack.GeneratedMessagePackResolver.AFormatter(),
+					_ => null, // unreachable
+				};
+			}
+
+			return null;
+		}
+	}
+}
+
+}
diff --git a/tests/MessagePack.SourceGenerator.Tests/Resources/NonDefaultDeserializingConstructor_Private/Formatters.A.AFormatter.g.cs b/tests/MessagePack.SourceGenerator.Tests/Resources/NonDefaultDeserializingConstructor_Private/Formatters.A.AFormatter.g.cs
new file mode 100644
index 000000000..5666b90b8
--- /dev/null
+++ b/tests/MessagePack.SourceGenerator.Tests/Resources/NonDefaultDeserializingConstructor_Private/Formatters.A.AFormatter.g.cs
@@ -0,0 +1,55 @@
+﻿// <auto-generated />
+
+#pragma warning disable 618, 612, 414, 168, CS1591, SA1129, SA1309, SA1312, SA1403, SA1649
+
+#pragma warning disable CS8669 // We may leak nullable annotations into generated code.
+
+using MsgPack = global::MessagePack;
+
+partial class A {
+
+	internal sealed class AFormatter : MsgPack::Formatters.IMessagePackFormatter<global::A>
+	{
+
+		public void Serialize(ref MsgPack::MessagePackWriter writer, global::A value, MsgPack::MessagePackSerializerOptions options)
+		{
+			if (value == null)
+			{
+				writer.WriteNil();
+				return;
+			}
+
+			writer.WriteArrayHeader(1);
+			writer.Write(value.X);
+		}
+
+		public global::A Deserialize(ref MsgPack::MessagePackReader reader, MsgPack::MessagePackSerializerOptions options)
+		{
+			if (reader.TryReadNil())
+			{
+				return null;
+			}
+
+			options.Security.DepthStep(ref reader);
+			var length = reader.ReadArrayHeader();
+			var __X__ = default(int);
+
+			for (int i = 0; i < length; i++)
+			{
+				switch (i)
+				{
+					case 0:
+						__X__ = reader.ReadInt32();
+						break;
+					default:
+						reader.Skip();
+						break;
+				}
+			}
+
+			var ____result = new global::A(__X__);
+			reader.Depth--;
+			return ____result;
+		}
+	}
+}
diff --git a/tests/MessagePack.SourceGenerator.Tests/Resources/NonDefaultDeserializingConstructor_Private/MessagePack.GeneratedMessagePackResolver.g.cs b/tests/MessagePack.SourceGenerator.Tests/Resources/NonDefaultDeserializingConstructor_Private/MessagePack.GeneratedMessagePackResolver.g.cs
new file mode 100644
index 000000000..5b00f9cf1
--- /dev/null
+++ b/tests/MessagePack.SourceGenerator.Tests/Resources/NonDefaultDeserializingConstructor_Private/MessagePack.GeneratedMessagePackResolver.g.cs
@@ -0,0 +1,63 @@
+﻿// <auto-generated />
+
+#pragma warning disable 618, 612, 414, 168, CS1591, SA1129, SA1309, SA1312, SA1403, SA1649
+
+using MsgPack = global::MessagePack;
+
+[assembly: MsgPack::Internal.GeneratedAssemblyMessagePackResolverAttribute(typeof(MessagePack.GeneratedMessagePackResolver), 3, 0)]
+
+namespace MessagePack {
+
+/// <summary>A MessagePack resolver that uses generated formatters for types in this assembly.</summary>
+partial class GeneratedMessagePackResolver : MsgPack::IFormatterResolver
+{
+	/// <summary>An instance of this resolver that only returns formatters specifically generated for types in this assembly.</summary>
+	public static readonly MsgPack::IFormatterResolver Instance = new GeneratedMessagePackResolver();
+
+	private GeneratedMessagePackResolver()
+	{
+	}
+
+	public MsgPack::Formatters.IMessagePackFormatter<T> GetFormatter<T>()
+	{
+		return FormatterCache<T>.Formatter;
+	}
+
+	private static class FormatterCache<T>
+	{
+		internal static readonly MsgPack::Formatters.IMessagePackFormatter<T> Formatter;
+
+		static FormatterCache()
+		{
+			var f = GeneratedMessagePackResolverGetFormatterHelper.GetFormatter(typeof(T));
+			if (f != null)
+			{
+				Formatter = (MsgPack::Formatters.IMessagePackFormatter<T>)f;
+			}
+		}
+	}
+
+	private static class GeneratedMessagePackResolverGetFormatterHelper
+	{
+		private static readonly global::System.Collections.Generic.Dictionary<global::System.Type, int> closedTypeLookup = new(1)
+		{
+			{ typeof(global::A), 0 },
+		};
+
+		internal static object GetFormatter(global::System.Type t)
+		{
+			if (closedTypeLookup.TryGetValue(t, out int closedKey))
+			{
+				return closedKey switch
+				{
+					0 => new global::A.AFormatter(),
+					_ => null, // unreachable
+				};
+			}
+
+			return null;
+		}
+	}
+}
+
+}