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

Thrown by Decoder.unmarshalCSV (parser/metadecoders/decoder.go:357) when TargetType is "map" and the CSV header row (first record) contains duplicate field names. Map-mode CSV turns each row into a map keyed by header names; duplicate names would collide/overwrite, so Hugo rejects it upfront. The %T reflects the target.

Solutions

  1. Make every header in the first row unique.
  2. If duplicate columns are intentional (e.g. multi-value), switch to TargetType="slice" and index columns positionally.
  3. Re-export the CSV from the source ensuring distinct column names.

Example fix

# before: data.csv (duplicate header)
name,name,value
foo,bar,1

# after: data.csv
name,label,value
foo,bar,1
Defensive patterns

Strategy: validation

Validate before calling

// Detect duplicate CSV headers for map mode.
func csvHeadersUnique(records [][]string) bool {
    seen := map[string]bool{}
    for _, h := range records[0] {
        if seen[h] { return false }
        seen[h] = true
    }
    return true
}

Try / catch

if err := dec.UnmarshalTo(data, metadecoders.CSV, &v); err != nil {
    if strings.Contains(err.Error(), "duplicate field names") {
        // rename duplicate headers upstream
    }
    return err
}

Prevention

When it happens

Trigger: A CSV whose first row has repeated column headers (e.g. name,name,value); an export tool that duplicates column titles; a hand-authored CSV with a copy-paste header mistake.

Common situations: Exporting from a spreadsheet with merged/duplicated headers; a data feed that repeats a column; accidental duplication when editing a CSV template.

Related errors


AI-assisted analysis of gohugoio/hugo@52c9bd7908 (2026-08-09). Data as JSON: /api/errors/71b31b437bc5b902. Report an issue: GitHub.

Appendix: 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 52c9bd7908)