gohugoio/hugo · error
cannot unmarshal CSV into %T: header row contains duplicate
Error message
cannot unmarshal CSV into %T: header row contains duplicate field names
What it means
When unmarshalling CSV with TargetType "map", each data row becomes a map[string]string keyed by header field names. Duplicate header names would silently overwrite earlier columns' values, so Hugo checks the header row with a seen-set and rejects any repetition rather than losing data.
Source
Thrown at parser/metadecoders/decoder.go:357
r.LazyQuotes = d.LazyQuotes
records, err := r.ReadAll()
if err != nil {
return err
}
switch vv := v.(type) {
case *any:
switch d.TargetType {
case "map":
if len(records) < 2 {
return fmt.Errorf("cannot unmarshal CSV into %T: expected at least a header row and one data row", v)
}
seen := make(map[string]bool, len(records[0]))
for _, fieldName := range records[0] {
if seen[fieldName] {
return fmt.Errorf("cannot unmarshal CSV into %T: header row contains duplicate field names", v)
}
seen[fieldName] = true
}
sm := make([]map[string]string, len(records)-1)
for i, record := range records[1:] {
m := make(map[string]string, len(records[0]))
for j, col := range record {
m[records[0][j]] = col
}
sm[i] = m
}
*vv = sm
case "slice":
*vv = records
default:
return fmt.Errorf("cannot unmarshal CSV into %T: invalid targetType: expected either slice or map, received %s", v, d.TargetType)
}View on GitHub ↗ (pinned to 8a468df065)
Solutions
- Rename the duplicate columns in the CSV so every header field is unique (e.g. date_start, date_end).
- Remove trailing empty columns/commas from the header row — multiple empty headers count as duplicates.
- If you cannot edit the source, unmarshal with targetType "slice" and index columns by position instead of by name.
Example fix
// before (header row) id,date,date,name // after id,date_created,date_modified,name
Defensive patterns
Strategy: validation
Validate before calling
r := csv.NewReader(bytes.NewReader(data))
header, err := r.Read()
if err != nil {
return err
}
seen := map[string]bool{}
for _, h := range header {
if seen[h] {
return fmt.Errorf("duplicate CSV column %q in %s", h, filename)
}
seen[h] = true
} Try / catch
if err := decoder.UnmarshalTo(data, metadecoders.CSV, &rows); err != nil {
if strings.Contains(err.Error(), "duplicate field names") {
// report the offending file and its header so the author can rename columns
}
return err
} Prevention
- Give every CSV column a unique, non-empty name
- Lint CSV headers in CI (a few lines of script) before content builds
- When exporting from spreadsheets, check for auto-generated duplicate columns like "Column1, Column1"
When it happens
Trigger: transform.Unmarshal with (dict "targetType" "map") on a CSV whose first row contains the same field name twice — including multiple empty-string headers (two blank columns) or case-sensitive duplicates that are byte-identical.
Common situations: Spreadsheet exports with repeated column names (e.g. two "Date" columns) or trailing empty columns producing multiple "" headers; wrong delimiter causing the whole header line to parse oddly; joined datasets exported without renaming clashing columns.
Related errors
- failed to decode options: %w
- invalid character: %q
- cannot unmarshal CSV into %T: expected at least a header row
- unmarshal takes 1 or 2 arguments
- first argument must be a map
AI-assisted analysis of gohugoio/hugo@8a468df065 (2026-07-31).
Data as JSON: /data/errors/71b31b437bc5b902.json.
Report an issue: GitHub ↗.