-
-
Notifications
You must be signed in to change notification settings - Fork 215
Description
Is your feature request related to a problem? Please describe.
When mapping from a nullable source property to a non-nullable init target property that has a default initializer, Mapperly always includes the property in the object initializer and generates ?? throw new ArgumentNullException(...) (or ?? default/?? new() with ThrowOnMappingNullMismatch = false).
For non-required init properties, C# allows omitting them from the object initializer entirely, which would let the target type's own initializer provide the default value. Mapperly doesn't take advantage of this; it treats all init properties as if they were required.
This is a problem when mapping from DTOs with all-nullable properties (common for deserialized config/YAML/JSON) to domain models that use non-nullable init properties with sensible defaults.
Describe the solution you'd like
When ThrowOnPropertyMappingNullMismatch is false (the default) and the target property is a non-required init property with a default initializer, Mapperly should omit the property from the object initializer when the source is null, preserving the target's default.
// Source
public record SourceDto
{
public string? Name { get; init; }
public IReadOnlyCollection<string>? Items { get; init; }
public bool? Enabled { get; init; }
}
// Target
public record Target
{
public string Name { get; init; } = "";
public IReadOnlyCollection<string> Items { get; init; } = [];
public bool Enabled { get; init; }
}
[Mapper]
public static partial class MyMapper
{
public static partial Target Map(SourceDto source);
}
// Current generated code (v4.3.1):
public static partial Target Map(SourceDto source)
{
var target = new Target()
{
Name = source.Name ?? throw new ArgumentNullException(nameof(source.Name)),
Items = source.Items ?? throw new ArgumentNullException(nameof(source.Items)),
Enabled = source.Enabled ?? throw new ArgumentNullException(nameof(source.Enabled)),
};
return target;
}
// Desired generated code:
public static partial Target Map(SourceDto source)
{
var target = new Target()
{
Name = source.Name ?? "", // or omit entirely
Items = source.Items ?? [], // or omit entirely
Enabled = source.Enabled ?? default, // or omit entirely
};
return target;
}Ideally, non-required init properties would simply be omitted from the initializer when the source is null, equivalent to how set properties get a conditional if (source.X != null) post-construction assignment. The target's own field initializer handles the default.
Describe alternatives you've considered
-
ThrowOnMappingNullMismatch = false: This changes the fallback fromthrowto?? default/?? new(), which partially helps. However, it fails for interface collection types (ICollection<T>,IReadOnlyCollection<T>) withRMG002because they have no parameterless constructor. It also doesn't truly "skip" the assignment; it substitutes a value that may differ from the target's initializer. -
Changing
inittoset: This works because Mapperly generates conditional post-construction assignments forsetproperties. But it sacrifices theinitcontract solely to work around Mapperly's code generation, which feels backwards. -
[MapperIgnoreTarget]: This always skips the property regardless of source value, which isn't the desired behavior (we want to map when the source is non-null).
Additional context
This is related to #1825 (closed without resolution). The docs state that AllowNullPropertyAssignment, ThrowOnPropertyMappingNullMismatch, and ThrowOnMappingNullMismatch are "ignored for required init properties," which implies non-required init properties should respect these settings. In practice, ThrowOnPropertyMappingNullMismatch (the "skip the assignment" behavior) has no effect on any init property, required or not.
The root cause appears to be in NewInstanceObjectMemberMappingBodyBuilder.BuildInitMemberMappings(), which uses CodeStyle.Expression for all init properties. Expression context can't contain if statements, but it could omit the property entirely from the initializer when the source is null, since C# only requires required properties to appear.