Repository: riok/mapperly (2026-06-16T09:47:15Z)

Issue:
Generated null checks fail to compile (CS9342) when the source type defines equality operators against its underlying primitive
**Please do the checklist before filing an issue:**
- [x] I have read the [documentation](https://mapperly.riok.app), including the [FAQ](https://mapperly.riok.app/docs/getting-started/faq/)
- [x] I can reproduce the bug using the latest prerelease version
- [x] I have searched existing [discussion](https://github.com/riok/mapperly/discussions) and [issue](https://github.com/riok/mapperly/issues) to avoid duplicates

**Describe the bug**

When mapping a nullable reference-type member whose type defines user-defined equality operators against another type (the common "strongly-typed value object" pattern: `==/!=(Vo, Vo)` **and** `==/!=(Vo, string)`), Mapperly emits a bare null check (`source.Member != null`) in the generated mapping. That expression is ambiguous for the compiler, both operator overloads are applicable to the `null` literal, so the generated code does not compile.

This affects every value-object library that generates primitive comparison operators (e.g. [Vogen](https://github.com/SteveDunn/Vogen), where `primitiveEqualityGeneration` is enabled by default), as well as hand-written value objects following the same pattern.

**Repro**

Self-contained declaration code below (no third-party dependency). I can push a repro repository on request.

**Declaration code**
```csharp
public class Code
{
    public string Value { get; init; } = "";

    public static explicit operator string(Code v) => v.Value;

    public static bool operator ==(Code? l, Code? r) => Equals(l, r);
    public static bool operator !=(Code? l, Code? r) => !Equals(l, r);
    public static bool operator ==(Code? l, string? r) => l?.Value == r;
    public static bool operator !=(Code? l, string? r) => !(l == r);

    public override bool Equals(object? o) => o is Code c && c.Value == Value;
    public override int GetHashCode() => Value.GetHashCode();
}

public class Order { public Code? PromoCode { get; init; } }
public record OrderDto { public string? PromoCode { get; init; } }

[Mapper]
public static partial class OrderMapper
{
    public static partial OrderDto ToDto(this Order source);
}
```

**Actual relevant generated code**
```csharp
var target = new OrderDto()
{
    PromoCode = source.PromoCode != null ? (string)source.PromoCode : default,
    //                           ^^^^^^^
    // error CS9342: Operator resolution is ambiguous between the following members:
    // 'Code.operator !=(Code?, Code?)' and 'Code.operator !=(Code?, string?)'
};
```

**Expected relevant generated code**
```csharp
var target = new OrderDto()
{
    PromoCode = source.PromoCode != (Code?)null ? (string)source.PromoCode : default,
};
```

Casting the null literal to the source type disambiguates overload resolution without changing anything else, and seems the most general fix:

- it is valid **both** in object (in-memory) mappings **and** inside queryable projection expression trees, so a single codegen shape fixes every context;
- it is behavior-preserving: where the bare `!= null` compiles today (types with a single applicable `==` overload), Mapperly already dispatches to the user-defined operator — the typed cast only disambiguates, it does not change which operator runs;
- in projections the operator is never executed anyway, the ORM translates the comparison to `IS NULL` / `IS NOT NULL`.

An alternative for object mappings only would be pattern matching (`source.PromoCode is not null`), but it is not valid inside expression trees and it silently changes semantics from operator dispatch to reference equality, so the typed-null cast seems the safer and more consistent fix.

**Reported relevant diagnostics**
* None from Mapperly, the failure is the C# compiler error `CS9342` reported on the generated file (`obj/.../Riok.Mapperly/...Mapper.g.cs`).

**Environment (please complete the following information):**
- Mapperly Version: 4.3.1 (also reproduced on 5.0.0-next.8)   
- Nullable reference types: enabled
- .NET Version: SDK 10.0.300
- Target Framework: net10.0
- Compiler Version: 5.6.0-2.26230.102 (caa81fa4971f74880cdab61990cb1b11420939ec)
- C# Language Version: latest (14.0)
- IDE: Rider 2026.1
- OS: Windows 11 Pro (10.0.26200)

**Additional context**

Workaround I use today: a user-implemented mapping with an explicitly typed null comparison, repeated for every nullable value-object type in every mapper:

```csharp
private static string? MapPromoCode(Code? code) => code == (Code?)null ? null : (string)code;
```

I am happy to contribute a PR for this if you agree with the direction.


Discussion:

Patch:
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullDelegateAssignmentMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullDelegateAssignmentMapping.cs
index 51adc2d6ec..76043202a2 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullDelegateAssignmentMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullDelegateAssignmentMapping.cs
@@ -36,7 +36,8 @@ public override IEnumerable<StatementSyntax> Build(TypeMappingBuildContext ctx,
             nullConditional: needsNullSafeAccess,
             skipTrailingNonNullable: true
         );
-        var condition = IsNotNull(sourceNullConditionalAccess);
+        var leafType = _nullConditionalSourcePath.MemberPath.ReadPathWithoutTrailingNonNullable().LastOrDefault()?.Type;
+        var condition = IsNotNull(sourceNullConditionalAccess, leafType);
         var conditionCtx = ctx.AddIndentation();
         var trueClause = base.Build(conditionCtx, targetAccess);
         var elseClause = BuildElseClause(conditionCtx, targetAccess);
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MethodMemberNullAssignmentInitializerMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MethodMemberNullAssignmentInitializerMapping.cs
index ea38744d0a..fd346af57b 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MethodMemberNullAssignmentInitializerMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MethodMemberNullAssignmentInitializerMapping.cs
@@ -26,7 +26,11 @@ public override IEnumerable<StatementSyntax> Build(TypeMappingBuildContext ctx,
         var setTarget = _targetPathToInitialize.BuildAssignment(targetAccess, newTarget);
 
         // if (target.Value == null) target.Value = new T();
-        var setTargetIfNull = ctx.SyntaxFactory.IfNull(sourcePathToInitialize.BuildAccess(targetAccess), setTarget);
+        var setTargetIfNull = ctx.SyntaxFactory.IfNull(
+            sourcePathToInitialize.BuildAccess(targetAccess),
+            setTarget,
+            sourcePathToInitialize.MemberPath.Member?.Type
+        );
 
         return base.Build(ctx, targetAccess).Prepend(setTargetIfNull);
     }
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMapping.cs
index 341ca8bdfe..5c6c70926f 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMapping.cs
@@ -92,6 +92,6 @@ public override ExpressionSyntax Build(TypeMappingBuildContext ctx)
             nonNullableSourceValue = MemberAccess(nonNullableSourceValue, NullableValueProperty);
         }
 
-        return Conditional(IsNull(ctx.Source), nullSubstitute, _delegateMapping.Build(ctx.WithSource(nonNullableSourceValue)));
+        return Conditional(IsNull(ctx.Source, SourceType), nullSubstitute, _delegateMapping.Build(ctx.WithSource(nonNullableSourceValue)));
     }
 }
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMethodMapping.cs
index ae18cf395b..6eed1bb078 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMethodMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMethodMapping.cs
@@ -61,7 +61,7 @@ private IEnumerable<StatementSyntax> AddPreNullHandling(TypeMappingBuildContext
         // if (source == null)
         //   return <null-substitute>;
         var fallbackExpression = NullSubstitute(TargetType, ctx.Source, nullFallbackValue);
-        var ifExpression = ctx.SyntaxFactory.IfNullReturnOrThrow(ctx.Source, fallbackExpression);
+        var ifExpression = ctx.SyntaxFactory.IfNullReturnOrThrow(ctx.Source, fallbackExpression, SourceType);
         return body.Prepend(ifExpression);
     }
 }
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetGenericTypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetGenericTypeMapping.cs
index f5dbf16067..6a28a721e9 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetGenericTypeMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetGenericTypeMapping.cs
@@ -226,12 +226,12 @@ private StatementSyntax BuildNullGuard(TypeMappingBuildContext ctx)
         var nullChecks = new List<ExpressionSyntax>();
         if (SourceType.IsNullable() || CanBeNull(SourceType))
         {
-            nullChecks.Add(IsNull(ctx.Source));
+            nullChecks.Add(IsNull(ctx.Source, SourceType));
         }
 
         if (TargetType.IsNullable() || CanBeNull(TargetType))
         {
-            nullChecks.Add(IsNull(IdentifierName(TargetParameter.Name)));
+            nullChecks.Add(IsNull(IdentifierName(TargetParameter.Name), TargetType));
         }
 
         return ctx.SyntaxFactory.If(Or(nullChecks), ctx.SyntaxFactory.AddIndentation().Return());
diff --git a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeFieldAccessor.cs b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeFieldAccessor.cs
index 8652333e4a..d8ac2b3990 100644
--- a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeFieldAccessor.cs
+++ b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeFieldAccessor.cs
@@ -64,7 +64,7 @@ public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, INamedTypeSymb
         if (!nullConditional)
             return invocation;
 
-        return Conditional(IsNotNull(baseAccess), invocation, DefaultLiteral());
+        return Conditional(IsNotNull(baseAccess, containingType ?? symbol.ContainingType), invocation, DefaultLiteral());
     }
 
     public ExpressionSyntax BuildAssignment(
diff --git a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeGetPropertyAccessor.cs b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeGetPropertyAccessor.cs
index 8e2c9d3bfa..8561124089 100644
--- a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeGetPropertyAccessor.cs
+++ b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeGetPropertyAccessor.cs
@@ -69,6 +69,6 @@ public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, INamedTypeSymb
         if (!nullConditional)
             return invocation;
 
-        return Conditional(IsNotNull(baseAccess), invocation, DefaultLiteral());
+        return Conditional(IsNotNull(baseAccess, containingType ?? symbol.ContainingType), invocation, DefaultLiteral());
     }
 }
diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Condition.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Condition.cs
index 4983f5377d..543d8df022 100644
--- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Condition.cs
+++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Condition.cs
@@ -59,21 +59,32 @@ public static ExpressionSyntax Equal(ExpressionSyntax left, ExpressionSyntax rig
 
     public static ExpressionSyntax IfNoneNull(params (ITypeSymbol Type, ExpressionSyntax Access)[] values)
     {
-        var conditions = values.Where(x => x.Type.IsNullable()).Select(x => IsNotNull(x.Access));
+        var conditions = values.Where(x => x.Type.IsNullable()).Select(x => IsNotNull(x.Access, x.Type));
         return And(conditions);
     }
 
     public static ExpressionSyntax IfAnyNull(params (ITypeSymbol Type, ExpressionSyntax Access)[] values)
     {
-        var conditions = values.Where(x => x.Type.IsNullable()).Select(x => IsNull(x.Access));
+        var conditions = values.Where(x => x.Type.IsNullable()).Select(x => IsNull(x.Access, x.Type));
         return Or(conditions);
     }
 
-    public static BinaryExpressionSyntax IsNull(ExpressionSyntax expression) =>
-        BinaryExpression(SyntaxKind.EqualsExpression, expression, NullLiteral());
-
-    public static BinaryExpressionSyntax IsNotNull(ExpressionSyntax expression) =>
-        BinaryExpression(SyntaxKind.NotEqualsExpression, expression, NullLiteral());
+    public static BinaryExpressionSyntax IsNull(ExpressionSyntax expression, ITypeSymbol? type = null) =>
+        BinaryExpression(SyntaxKind.EqualsExpression, expression, NullLiteral(type, WellKnownMemberNames.EqualityOperatorName));
+
+    public static BinaryExpressionSyntax IsNotNull(ExpressionSyntax expression, ITypeSymbol? type = null) =>
+        BinaryExpression(SyntaxKind.NotEqualsExpression, expression, NullLiteral(type, WellKnownMemberNames.InequalityOperatorName));
+
+    /// <summary>
+    /// Builds a <c>null</c> literal. If comparing <paramref name="type"/> against <c>null</c> using the operator
+    /// <paramref name="operatorMetadataName"/> would be ambiguous (multiple user defined overloads), the literal is
+    /// cast to the nullable type (e.g. <c>(global::Code?)null</c>) to avoid an ambiguous operator resolution (CS9342)
+    /// in the generated null check. See https://github.com/riok/mapperly/issues/2316.
+    /// </summary>
+    private static ExpressionSyntax NullLiteral(ITypeSymbol? type, string operatorMetadataName) =>
+        type?.HasAmbiguousNullComparisonOperator(operatorMetadataName) == true
+            ? CastExpression(NullableType(NonNullableIdentifier(type)), NullLiteral())
+            : NullLiteral();
 
     public static BinaryExpressionSyntax Is(ExpressionSyntax left, ExpressionSyntax right) =>
         BinaryExpression(SyntaxKind.IsExpression, left, right);
diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Null.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Null.cs
index 74a8cd11cf..23436e2520 100644
--- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Null.cs
+++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Null.cs
@@ -60,12 +60,17 @@ NullFallbackValue.Default when t.IsNullableValueType() => DefaultExpression(Full
         };
     }
 
-    public IfStatementSyntax IfNull(ExpressionSyntax value, ExpressionSyntax body) =>
-        IfNull(value, AddIndentation().ExpressionStatement(body));
+    public IfStatementSyntax IfNull(ExpressionSyntax value, ExpressionSyntax body, ITypeSymbol? type = null) =>
+        IfNull(value, AddIndentation().ExpressionStatement(body), type);
 
-    public IfStatementSyntax IfNull(ExpressionSyntax value, StatementSyntax statement) => If(IsNull(value), statement);
+    public IfStatementSyntax IfNull(ExpressionSyntax value, StatementSyntax statement, ITypeSymbol? type = null) =>
+        If(IsNull(value, type), statement);
 
-    public StatementSyntax IfNullReturnOrThrow(ExpressionSyntax expression, ExpressionSyntax? returnOrThrowExpression = null)
+    public StatementSyntax IfNullReturnOrThrow(
+        ExpressionSyntax expression,
+        ExpressionSyntax? returnOrThrowExpression = null,
+        ITypeSymbol? type = null
+    )
     {
         StatementSyntax ifExpression = returnOrThrowExpression switch
         {
@@ -73,7 +78,7 @@ public StatementSyntax IfNullReturnOrThrow(ExpressionSyntax expression, Expressi
             _ => AddIndentation().Return(returnOrThrowExpression),
         };
 
-        return IfNull(expression, ifExpression);
+        return IfNull(expression, ifExpression, type);
     }
 
     public static ExpressionSyntax SuppressNullableWarning(ExpressionSyntax expression) =>
diff --git a/src/Riok.Mapperly/Helpers/SymbolExtensions.cs b/src/Riok.Mapperly/Helpers/SymbolExtensions.cs
index 07af34e665..e4f8235414 100644
--- a/src/Riok.Mapperly/Helpers/SymbolExtensions.cs
+++ b/src/Riok.Mapperly/Helpers/SymbolExtensions.cs
@@ -80,6 +80,32 @@ internal static int GetInheritanceLevel(this ITypeSymbol symbol)
         return level;
     }
 
+    /// <summary>
+    /// Whether comparing the type against the bare <c>null</c> literal using the given operator can be ambiguous
+    /// (CS9342). This is the case when the type declares multiple user defined overloads of the operator,
+    /// e.g. when both <c>==(T?, T?)</c> and <c>==(T?, string?)</c> are defined, since the <c>null</c> literal is
+    /// convertible to multiple operand types. A single overload (as defined by e.g. <see langword="string"/>) is
+    /// never ambiguous.
+    /// The operator may be declared on a base type; per the C# specification only the first type in the hierarchy
+    /// declaring the operator contributes candidates, so the hierarchy is walked until the operator is found.
+    /// </summary>
+    /// <param name="symbol">The type which is compared against <c>null</c>.</param>
+    /// <param name="operatorMetadataName">
+    /// The metadata name of the operator, e.g. <see cref="WellKnownMemberNames.EqualityOperatorName"/> for <c>==</c>
+    /// or <see cref="WellKnownMemberNames.InequalityOperatorName"/> for <c>!=</c>.
+    /// </param>
+    internal static bool HasAmbiguousNullComparisonOperator(this ITypeSymbol symbol, string operatorMetadataName)
+    {
+        foreach (var type in symbol.NonNullable().WalkTypeHierarchy())
+        {
+            var count = type.GetMembers(operatorMetadataName).Count(m => m is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator });
+            if (count > 0)
+                return count > 1;
+        }
+
+        return false;
+    }
+
     internal static bool IsArrayType(this ITypeSymbol symbol) => symbol.TypeKind == TypeKind.Array;
 
     internal static bool IsArrayType(this ITypeSymbol symbol, [NotNullWhen(true)] out IArrayTypeSymbol? arrayTypeSymbol)
diff --git a/src/Riok.Mapperly/Symbols/Members/MemberPathGetter.cs b/src/Riok.Mapperly/Symbols/Members/MemberPathGetter.cs
index 2a44bc7938..5d3802b74c 100644
--- a/src/Riok.Mapperly/Symbols/Members/MemberPathGetter.cs
+++ b/src/Riok.Mapperly/Symbols/Members/MemberPathGetter.cs
@@ -89,7 +89,8 @@ public static MemberPathGetter Build(SimpleMappingBuilderContext ctx, MemberPath
 
     private BinaryExpressionSyntax BuildNonNullCondition(ExpressionSyntax baseAccess)
     {
-        return IsNotNull(BuildAccess(baseAccess, nullConditional: true, skipTrailingNonNullable: true));
+        var leafType = ReadPathWithoutTrailingNonNullable().LastOrDefault().Member?.Type;
+        return IsNotNull(BuildAccess(baseAccess, nullConditional: true, skipTrailingNonNullable: true), leafType);
     }
 
     private ExpressionSyntax? BuildNonNullConditionWithoutConditionalAccess(ExpressionSyntax baseAccess)
@@ -104,7 +105,7 @@ private BinaryExpressionSyntax BuildNonNullCondition(ExpressionSyntax baseAccess
             if (!pathPart.Member.IsReadNullable)
                 continue;
 
-            conditions.Add(IsNotNull(access));
+            conditions.Add(IsNotNull(access, pathPart.Member.Type));
         }
 
         return conditions.Count == 0 ? null : And(conditions);


Test patch:
diff --git a/test/Riok.Mapperly.Tests/Mapping/CastTest.cs b/test/Riok.Mapperly.Tests/Mapping/CastTest.cs
index ab0d9083b5..9e9c405d08 100644
--- a/test/Riok.Mapperly.Tests/Mapping/CastTest.cs
+++ b/test/Riok.Mapperly.Tests/Mapping/CastTest.cs
@@ -419,4 +419,129 @@ public void FromObjectExplicitCastShouldBeIgnoredAndDiagnostic()
             .HaveDiagnostic(DiagnosticDescriptors.CouldNotCreateMapping)
             .HaveAssertedAllDiagnostics();
     }
+
+    [Fact]
+    public void NullableSourceWithMultipleUserDefinedEqualityOperatorsShouldCastNullLiteral()
+    {
+        // https://github.com/riok/mapperly/issues/2316
+        // the null check needs an explicit cast of the null literal,
+        // otherwise the user defined equality operators lead to an ambiguous operator resolution (CS9342).
+        var source = TestSourceBuilder.Mapping(
+            "A",
+            "B",
+            TestSourceBuilderOptions.AllConversions,
+            "class A { public Code? PromoCode { get; set; } }",
+            "class B { public string? PromoCode { get; set; } }",
+            """
+            class Code
+            {
+                public string Value { get; set; } = string.Empty;
+                public static bool operator ==(Code? a, Code? b) => Equals(a, b);
+                public static bool operator !=(Code? a, Code? b) => !Equals(a, b);
+                public static bool operator ==(Code? a, string? b) => a?.Value == b;
+                public static bool operator !=(Code? a, string? b) => a?.Value != b;
+                public static explicit operator string(Code c) => c.Value;
+                public override bool Equals(object? o) => o is Code c && c.Value == Value;
+                public override int GetHashCode() => Value.GetHashCode();
+            }
+            """
+        );
+
+        TestHelper
+            .GenerateMapper(source)
+            .Should()
+            .HaveSingleMethodBody(
+                """
+                var target = new global::B();
+                if (source.PromoCode != (global::Code?)null)
+                {
+                    target.PromoCode = (string)source.PromoCode;
+                }
+                else
+                {
+                    target.PromoCode = null;
+                }
+                return target;
+                """
+            );
+    }
+
+    [Fact]
+    public Task NullableSourceWithMultipleUserDefinedEqualityOperatorsShouldCastNullLiteralInProjection()
+    {
+        // https://github.com/riok/mapperly/issues/2316
+        // the cast also needs to be emitted for queryable projection expressions.
+        var source = TestSourceBuilder.Mapping(
+            "System.Linq.IQueryable<A>",
+            "System.Linq.IQueryable<B>",
+            TestSourceBuilderOptions.AllConversions,
+            "class A { public Code? PromoCode { get; set; } }",
+            "class B { public string? PromoCode { get; set; } }",
+            """
+            class Code
+            {
+                public string Value { get; set; } = string.Empty;
+                public static bool operator ==(Code? a, Code? b) => Equals(a, b);
+                public static bool operator !=(Code? a, Code? b) => !Equals(a, b);
+                public static bool operator ==(Code? a, string? b) => a?.Value == b;
+                public static bool operator !=(Code? a, string? b) => a?.Value != b;
+                public static explicit operator string(Code c) => c.Value;
+                public override bool Equals(object? o) => o is Code c && c.Value == Value;
+                public override int GetHashCode() => Value.GetHashCode();
+            }
+            """
+        );
+
+        return TestHelper.VerifyGenerator(source);
+    }
+
+    [Fact]
+    public void NullableSourceWithInheritedEqualityOperatorsShouldCastNullLiteral()
+    {
+        // https://github.com/riok/mapperly/issues/2316
+        // the ambiguous equality operators may be declared on a base type.
+        var source = TestSourceBuilder.Mapping(
+            "A",
+            "B",
+            TestSourceBuilderOptions.AllConversions,
+            "class A { public Code? PromoCode { get; set; } }",
+            "class B { public string? PromoCode { get; set; } }",
+            """
+            abstract class ValueObject
+            {
+                public static bool operator ==(ValueObject? a, ValueObject? b) => Equals(a, b);
+                public static bool operator !=(ValueObject? a, ValueObject? b) => !Equals(a, b);
+                public static bool operator ==(ValueObject? a, string? b) => a?.ToString() == b;
+                public static bool operator !=(ValueObject? a, string? b) => a?.ToString() != b;
+                public override bool Equals(object? o) => ReferenceEquals(this, o);
+                public override int GetHashCode() => 0;
+            }
+            """,
+            """
+            class Code : ValueObject
+            {
+                public string Value { get; set; } = string.Empty;
+                public static explicit operator string(Code c) => c.Value;
+            }
+            """
+        );
+
+        TestHelper
+            .GenerateMapper(source)
+            .Should()
+            .HaveSingleMethodBody(
+                """
+                var target = new global::B();
+                if (source.PromoCode != (global::Code?)null)
+                {
+                    target.PromoCode = (string)source.PromoCode;
+                }
+                else
+                {
+                    target.PromoCode = null;
+                }
+                return target;
+                """
+            );
+    }
 }
diff --git a/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs b/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs
index a66673e7b4..bd80a2ae16 100644
--- a/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs
+++ b/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs
@@ -18,6 +18,36 @@ public Task PrivateProperty()
         return TestHelper.VerifyGenerator(source);
     }
 
+    [Fact]
+    public Task PrivateNestedNullablePropertyWithAmbiguousEqualityOperators()
+    {
+        // https://github.com/riok/mapperly/issues/2316
+        // the null check of the generic unsafe accessor receiver needs the cast too.
+        var source = TestSourceBuilder.MapperWithBodyAndTypes(
+            """
+            [MapProperty("nested.Value", "value")]
+            partial B Map(A source);
+            """,
+            TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.All),
+            "class A { public Code<int>? nested { get; set; } }",
+            "class B { public int value { get; set; } }",
+            """
+            class Code<T>
+            {
+                private T Value { get; set; } = default!;
+                public static bool operator ==(Code<T>? a, Code<T>? b) => Equals(a, b);
+                public static bool operator !=(Code<T>? a, Code<T>? b) => !Equals(a, b);
+                public static bool operator ==(Code<T>? a, string? b) => a?.ToString() == b;
+                public static bool operator !=(Code<T>? a, string? b) => a?.ToString() != b;
+                public override bool Equals(object? o) => ReferenceEquals(this, o);
+                public override int GetHashCode() => 0;
+            }
+            """
+        );
+
+        return TestHelper.VerifyGenerator(source);
+    }
+
     [Fact]
     public Task PrivatePropertyInNamespacedNestedMapper()
     {
diff --git a/test/Riok.Mapperly.Tests/_snapshots/CastTest.NullableSourceWithMultipleUserDefinedEqualityOperatorsShouldCastNullLiteralInProjection#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/CastTest.NullableSourceWithMultipleUserDefinedEqualityOperatorsShouldCastNullLiteralInProjection#Mapper.g.verified.cs
new file mode 100644
index 0000000000..0f99723204
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/CastTest.NullableSourceWithMultipleUserDefinedEqualityOperatorsShouldCastNullLiteralInProjection#Mapper.g.verified.cs
@@ -0,0 +1,19 @@
+﻿//HintName: Mapper.g.cs
+// <auto-generated />
+#nullable enable
+public partial class Mapper
+{
+    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+    private partial global::System.Linq.IQueryable<global::B> Map(global::System.Linq.IQueryable<global::A> source)
+    {
+#nullable disable
+        return global::System.Linq.Queryable.Select(
+            source,
+            x => new global::B()
+            {
+                PromoCode = x.PromoCode != (global::Code?)null ? (string)x.PromoCode : default,
+            }
+        );
+#nullable enable
+    }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateNestedNullablePropertyWithAmbiguousEqualityOperators#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateNestedNullablePropertyWithAmbiguousEqualityOperators#Mapper.g.verified.cs
new file mode 100644
index 0000000000..8d6c62366d
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateNestedNullablePropertyWithAmbiguousEqualityOperators#Mapper.g.verified.cs
@@ -0,0 +1,24 @@
+﻿//HintName: Mapper.g.cs
+// <auto-generated />
+#nullable enable
+public partial class Mapper
+{
+    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+    partial global::B Map(global::A source)
+    {
+        var target = new global::B();
+        if (source.nested != (global::Code<int>?)null)
+        {
+            target.value = CodeAccessor<int>.GetValue(source.nested);
+        }
+        return target;
+    }
+}
+
+[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+static file class CodeAccessor<T>
+{
+    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+    [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_Value")]
+    public static extern T GetValue(global::Code<T> source);
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateNestedNullablePropertyWithAmbiguousEqualityOperators.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateNestedNullablePropertyWithAmbiguousEqualityOperators.verified.txt
new file mode 100644
index 0000000000..4a893d0214
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateNestedNullablePropertyWithAmbiguousEqualityOperators.verified.txt
@@ -0,0 +1,26 @@
+﻿{
+  Diagnostics: [
+    {
+      Location: /*
+{
+    [MapProperty("nested.Value", "value")]
+    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+partial B Map(A source);
+^^^^^^^^^^^^^^^^^^^^^^^^
+}
+*/
+ : (11,4)-(12,24),
+      Message: Mapping the nullable source property nested.Value of A to the target property value of B which is not nullable,
+      Severity: Info,
+      WarningLevel: 1,
+      Descriptor: {
+        Id: RMG089,
+        Title: Mapping nullable source to non-nullable target member,
+        MessageFormat: Mapping the nullable source property {0} of {1} to the target property {2} of {3} which is not nullable,
+        Category: Mapper,
+        DefaultSeverity: Info,
+        IsEnabledByDefault: true
+      }
+    }
+  ]
+}
\ No newline at end of file