{"record":{"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/52c9bd7908b4d02d4d0ff8f82a888834d6ee10d2/parser/metadecoders/decoder.go#L333-L369","documentation":"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{}).","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure the CSV has at least one header row plus one or more data rows when using map mode.","If empty data is legitimate, use TargetType=\"slice\" which tolerates fewer rows, or handle the empty case before decoding.","Add a sentinel/placeholder data row, or skip decoding when the file is empty."],"exampleFix":"# before: data.csv (header only)\nname,value\n\n# after: data.csv\nname,value\nfoo,1","handlingStrategy":"validation","validationCode":"// Ensure CSV has header + at least one row for map mode.\nfunc csvHasMapData(records [][]string) bool {\n    return len(records) >= 2\n}","typeGuard":null,"tryCatchPattern":"if err := dec.UnmarshalTo(data, metadecoders.CSV, &v); err != nil {\n    if strings.Contains(err.Error(), \"at least a header row\") {\n        // switch to slice mode or skip empty file\n    }\n    return err\n}","preventionTips":["Use slice mode when CSV may be header-only/empty.","Skip decoding for empty files.","Validate row counts for map-mode CSV."],"tags":["csv","metadecoders","data","hugo"],"backgroundTag":null,"analyzedSha":"52c9bd7908b4d02d4d0ff8f82a888834d6ee10d2","analyzedAt":"2026-08-09T21:49:36.660Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}