C# – Using CsvHelper when there’s no header row

When you’re parsing CSV with CsvHelper and there’s no header row, you have to configure it to map by field position. I’ll show how to do that. At the end, I’ll show the alternative approach of manually parsing in this scenario.

Consider the following CSV data without a header row:

Inception,2010,Christopher Nolan
Pulp Fiction,1994,Quentin Tarantino
Jurassic Park,1993,Steven Spielberg
Code language: plaintext (plaintext)

Normally, CsvHelper maps fields to properties by matching column names from the header row to property names. But when there’s no header row, it has to map based on field position. It tries to match field positions to reflected property positions, but it’s not a good idea to rely on this (because there is no guaranteed ordering when using reflection). Instead, it’s better to explicitly define which field positions map to which properties. The simplest way to configure that it by using the CsvHelper [Index] attribute.

The fields in the CSV above are in this order: Title, Year, Director. So here’s how to use the [Index] attribute on the properties to match this field ordering:

using CsvHelper.Configuration.Attributes;

public class Movie
{   
    [Index(2)]
    public string Director { get; set; } 

    [Index(0)]
    public string Title { get; set; }

    [Index(1)]
    public int Year { get; set; }
}
Code language: C# (cs)

By default, CsvHelper assumes there’s a header row. You need to configure it to not expect a header row by setting CsvConfiguration.HasHeaderRecord=false when parsing, like this:

using System.Globalization;
using CsvHelper;
using CsvHelper.Configuration;

var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
    HasHeaderRecord = false
};

using (var reader = new StreamReader(@"C:\temp\movies.csv"))
using (var csv = new CsvReader(reader, config))
{
    foreach (var movie in csv.GetRecords<Movie>())
    {
        //Process deserialized records

        Console.WriteLine($"Movie: {movie.Title} ({movie.Year}, {movie.Director})");
    }
}
Code language: C# (cs)

Note: If you don’t set HasHeaderRecord to false, you are virtually guaranteed to run into a CsvHelper.HeaderValidationException, because it will try to use the data from the first row as if they were column names.

This outputs the following, showing that it properly dealt with this scenario of having no header row, and used the [Index] attributes to map by position:

Movie: Inception (2010, Christopher Nolan)
Movie: Pulp Fiction (1994, Quentin Tarantino)
Movie: Jurassic Park (1993, Steven Spielberg)Code language: plaintext (plaintext)

Simplify by parsing manually

When you manually parse CSV, you already know the field positions and already have to hardcode the mappings based on position (i.e. movie.Title = fields[0]). In this “no header row” scenario, consider parsing manually instead of using CsvHelper (unless you need the advanced CsvHelper features). This can simplify things.

Here’s an example of manually parsing the movie CSV data:

foreach (var line in System.IO.File.ReadLines(@"C:\temp\movies.csv"))
{
    var fields = line.Split(",");

    var movie = new Movie()
    {
        Title = fields[0],
        Year = Convert.ToInt32(fields[1]),
        Director = fields[2]
    };

    //process it

    Console.WriteLine($"Movie: {movie.Title} ({movie.Year}, {movie.Director})");
}
Code language: C# (cs)

Note: This is also a good way to deal with the scenario where you can’t change the model class (otherwise you have to do an even more complicated configuration by using CsvHelper’s ClassMap).

This outputs:

Movie: Inception (2010, Christopher Nolan)
Movie: Pulp Fiction (1994, Quentin Tarantino)
Movie: Jurassic Park (1993, Steven Spielberg)Code language: plaintext (plaintext)

Leave a Comment