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

  1. Rename the duplicate columns in the CSV so every header field is unique (e.g. date_start, date_end).
  2. Remove trailing empty columns/commas from the header row — multiple empty headers count as duplicates.
  3. 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

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


AI-assisted analysis of gohugoio/hugo@8a468df065 (2026-07-31). Data as JSON: /data/errors/71b31b437bc5b902.json. Report an issue: GitHub ↗.