{"id":"aeaf222881b021f1","repo":"gohugoio/hugo","slug":"cannot-unmarshal-csv-into-t-expected-at-least-a","errorCode":null,"errorMessage":"cannot unmarshal CSV into %T: expected at least a header row and one data row","messagePattern":"cannot unmarshal CSV into %T: expected at least a header row and one data row","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"parser/metadecoders/decoder.go","lineNumber":351,"sourceCode":"}\n\nfunc (d Decoder) unmarshalCSV(data []byte, v any) error {\n\tr := csv.NewReader(bytes.NewReader(data))\n\tr.Comma = d.Delimiter\n\tr.Comment = d.Comment\n\tr.LazyQuotes = d.LazyQuotes\n\n\trecords, err := r.ReadAll()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch vv := v.(type) {\n\tcase *any:\n\t\tswitch d.TargetType {\n\t\tcase \"map\":\n\t\t\tif len(records) < 2 {\n\t\t\t\treturn fmt.Errorf(\"cannot unmarshal CSV into %T: expected at least a header row and one data row\", v)\n\t\t\t}\n\n\t\t\tseen := make(map[string]bool, len(records[0]))\n\t\t\tfor _, fieldName := range records[0] {\n\t\t\t\tif seen[fieldName] {\n\t\t\t\t\treturn fmt.Errorf(\"cannot unmarshal CSV into %T: header row contains duplicate field names\", v)\n\t\t\t\t}\n\t\t\t\tseen[fieldName] = true\n\t\t\t}\n\n\t\t\tsm := make([]map[string]string, len(records)-1)\n\t\t\tfor i, record := range records[1:] {\n\t\t\t\tm := make(map[string]string, len(records[0]))\n\t\t\t\tfor j, col := range record {\n\t\t\t\t\tm[records[0][j]] = col\n\t\t\t\t}\n\t\t\t\tsm[i] = m\n\t\t\t}","sourceCodeStart":333,"sourceCodeEnd":369,"githubUrl":"https://github.com/gohugoio/hugo/blob/8a468df065a75c1c7cf9f6850f32148746590ea5/parser/metadecoders/decoder.go#L333-L369","documentation":"In Decoder.unmarshalCSV with TargetType \"map\", Hugo converts CSV rows into []map[string]string keyed by the header row. That requires at least two records: one header row plus one data row. Fewer than two parsed records makes the map construction meaningless, so Hugo fails fast instead of returning an empty or ambiguous result.","triggerScenarios":"transform.Unmarshal on a CSV resource with (dict \"targetType\" \"map\") when csv.ReadAll yields 0 or 1 records — an empty file, a header-only file, or a file whose delimiter option doesn't match the actual separator so everything collapses into one record.","commonSituations":"CSV exports that legitimately have zero data rows (empty query results) fed to a template expecting map output; wrong \"delimiter\" option (e.g. semicolon-separated file read with the default comma); files containing only a trailing newline; remote CSV endpoints returning an empty body.","solutions":["Ensure the CSV source has a header row plus at least one data row, or guard the template to skip unmarshalling when the resource content is empty/header-only.","If the file uses a non-comma separator, pass the matching option: transform.Unmarshal (dict \"delimiter\" \";\" \"targetType\" \"map\").","Use targetType \"slice\" (the default) if you must accept header-only files and handle the raw records yourself."],"exampleFix":"// before\n{{ $rows := $csv | transform.Unmarshal (dict \"targetType\" \"map\") }}\n// after\n{{ $rows := slice }}\n{{ if gt (len $csv.Content) 0 }}\n  {{ $rows = $csv | transform.Unmarshal (dict \"delimiter\" \";\" \"targetType\" \"map\") }}\n{{ end }}","handlingStrategy":"validation","validationCode":"// require header + at least one data row before decoding CSV into structs/maps\nrecords, err := csv.NewReader(bytes.NewReader(data)).ReadAll()\nif err != nil {\n    return err\n}\nif len(records) < 2 {\n    return fmt.Errorf(\"CSV %s needs a header row and at least one data row\", filename)\n}","typeGuard":null,"tryCatchPattern":"if err := decoder.UnmarshalTo(data, metadecoders.CSV, &rows); err != nil {\n    if strings.Contains(err.Error(), \"expected at least a header row\") {\n        // empty or header-only file — treat as no data, not corruption\n    }\n    return err\n}","preventionTips":["Never ship empty or header-only CSV data files; validate row count in CI","If a dataset can legitimately be empty, decode into [][]string instead of a keyed type, or skip the file when it has fewer than 2 rows","Watch for trailing exports that produce header-only CSVs from upstream tools"],"tags":["hugo","csv","unmarshal","data-files"],"analyzedSha":"8a468df065a75c1c7cf9f6850f32148746590ea5","analyzedAt":"2026-07-31T21:23:07.045Z","schemaVersion":2}