CsvHelper icon indicating copy to clipboard operation
CsvHelper copied to clipboard

Certain C# record types' optional fields cannot be ignored in ClassMaps

Open ogiarch opened this issue 6 months ago • 2 comments

I am experiencing this on version 33.0.1, but I don't see anything about this in the changelog for the latest version, so I believe it's likely still present there too. Please correct me if I am wrong, or if there's any preferred workaround for this issue.

Describe the bug When reading CSV data into C# record types declared in typical shorthand with positional constructors and optional fields, ClassMap objects cannot properly use Map(m => m.OptionalProperty).Ignore() to stop requiring that those fields be present in CSV files. The Ignore = true value successfully makes it into the ClassMap's MemberMaps, but not its ParameterMaps for each such OptionalProperty. Therefore, when CsvReader.ValidateHeader is invoked in the process of reading a CSV without headers for the ignored fields, the absent fields that should be ignored are counterintuitively still added to invalidHeaders and we end up with a HeaderValidationException. This is quite inconvenient for my use case, and seems like it may well be unintentional behavior.

This issue can be suppressed by placing [Ignore] before the names of the affected fields in the record type's primary constructor, but this is not an acceptable workaround for me, as I want certain fields to be ignored in certain contexts and required in other contexts (and I'd think this would be exactly what ClassMaps are intended to allow us to do).

To Reproduce example.csv:

RequiredStr,RequiredInt
"hello",2

example.cs:

...
public record CsvClass(
    string RequiredStr,
    int RequiredInt,
    string? OptionalStr = null);

private sealed class ExampleMap : ClassMap<CsvClass>
{
    public ExampleMap()
    {
        AutoMap(CultureInfo.InvariantCulture);
        Map(m => m.OptionalStr).Ignore();
    }
}

public List<CsvClass> ReadFromCsvExample()
{
    using var streamReader = new StreamReader(File.Open(".../example.cs", FileMode.Open));
    using var csvReader = new CsvReader(streamReader, CultureInfo.InvariantCulture);
    csvReader.Context.RegisterClassMap<ExampleMap>();
    return csvReader.GetRecords<CsvClass>().ToList();
}

Expected behavior Calling Map(m => m.x).Ignore() in the constructor of a ClassMap<T> object, where T is any record object with an optional parameter x in its default constructor, should properly cause the property x to be ignored when it is absent from a CSV, and the default value supplied for it to be used in the main constructor for T.

ogiarch avatar Jun 18 '25 18:06 ogiarch

There's one more workaround that I've noticed. If I were to instead define CsvClass in the above example like so, there's no problem:

public record CsvClass
{
    public required string RequiredStr { get; init; }
    public required int RequiredInt { get; init; }
    public string? OptionalStr { get; init; }
}

Given that this works, and the above example doesn't, this really does seem like a bug to me unless I'm missing something obvious.

ogiarch avatar Jun 18 '25 19:06 ogiarch

You need to either use the Parameter method.

class ExamplemMap : ClassMap<CsvClass>
{
	public ExamplemMap()
	{
		Parameter(nameof(CsvClass.RequiredStr));
		Parameter(nameof(CsvClass.RequiredInt));
		Parameter(nameof(CsvClass.OptionsStr)).Optional();
	}
}

Or you can use attributes.

record CsvClass(string RequiredStr, int RequiredInt, [param: Optional][property: Optional]string? OptionsStr);

I don't think you should have to use the param/property parts to the attributes, so I'll have to look into that part.

JoshClose avatar Jun 23 '25 20:06 JoshClose