gohugoio/hugo · error

cannot unmarshal CSV into %T: expected at least a header…

Error message

cannot unmarshal CSV into %T: expected at least a header row and one data row

What it means

Thrown by Decoder.unmarshalCSV (parser/metadecoders/decoder.go:351) when TargetType is "map" but the CSV has fewer than 2 records. Map-mode CSV uses the first row as field-name headers and subsequent rows as data; with zero or one row there is no data to map, so Hugo refuses rather than returning an empty/ambiguous result. The %T reflects the target (typically *interface{}).

Solutions

  1. Ensure the CSV has at least one header row plus one or more data rows when using map mode.
  2. If empty data is legitimate, use TargetType="slice" which tolerates fewer rows, or handle the empty case before decoding.
  3. Add a sentinel/placeholder data row, or skip decoding when the file is empty.

Example fix

# before: data.csv (header only)
name,value

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

Strategy: validation

Validate before calling

// Ensure CSV has header + at least one row for map mode.
func csvHasMapData(records [][]string) bool {
    return len(records) >= 2
}

Try / catch

if err := dec.UnmarshalTo(data, metadecoders.CSV, &v); err != nil {
    if strings.Contains(err.Error(), "at least a header row") {
        // switch to slice mode or skip empty file
    }
    return err
}

Prevention

When it happens

Trigger: Loading a .csv data file with TargetType="map" that contains only a header row or is completely empty; a CSV resource referenced via transform.Unmarshal configured for map output but lacking data rows.

Common situations: An empty CSV export; a header-only template CSV; a feed/resource that occasionally returns no rows; misconfiguring a CSV shortcode/resource with map target type.

Related errors


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

Appendix: source

Thrown at parser/metadecoders/decoder.go:351

}

func (d Decoder) unmarshalCSV(data []byte, v any) error {
	r := csv.NewReader(bytes.NewReader(data))
	r.Comma = d.Delimiter
	r.Comment = d.Comment
	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
			}

View on GitHub (pinned to 52c9bd7908)