{"id":"71b31b437bc5b902","repo":"gohugoio/hugo","slug":"cannot-unmarshal-csv-into-t-header-row-contains","errorCode":null,"errorMessage":"cannot unmarshal CSV into %T: header row contains duplicate field names","messagePattern":"cannot unmarshal CSV into %T: header row contains duplicate field names","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"parser/metadecoders/decoder.go","lineNumber":357,"sourceCode":"\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}\n\t\t\t*vv = sm\n\t\tcase \"slice\":\n\t\t\t*vv = records\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"cannot unmarshal CSV into %T: invalid targetType: expected either slice or map, received %s\", v, d.TargetType)\n\t\t}","sourceCodeStart":339,"sourceCodeEnd":375,"githubUrl":"https://github.com/gohugoio/hugo/blob/8a468df065a75c1c7cf9f6850f32148746590ea5/parser/metadecoders/decoder.go#L339-L375","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Rename the duplicate columns in the CSV so every header field is unique (e.g. date_start, date_end).","Remove trailing empty columns/commas from the header row — multiple empty headers count as duplicates.","If you cannot edit the source, unmarshal with targetType \"slice\" and index columns by position instead of by name."],"exampleFix":"// before (header row)\nid,date,date,name\n// after\nid,date_created,date_modified,name","handlingStrategy":"validation","validationCode":"r := csv.NewReader(bytes.NewReader(data))\nheader, err := r.Read()\nif err != nil {\n    return err\n}\nseen := map[string]bool{}\nfor _, h := range header {\n    if seen[h] {\n        return fmt.Errorf(\"duplicate CSV column %q in %s\", h, filename)\n    }\n    seen[h] = true\n}","typeGuard":null,"tryCatchPattern":"if err := decoder.UnmarshalTo(data, metadecoders.CSV, &rows); err != nil {\n    if strings.Contains(err.Error(), \"duplicate field names\") {\n        // report the offending file and its header so the author can rename columns\n    }\n    return err\n}","preventionTips":["Give every CSV column a unique, non-empty name","Lint CSV headers in CI (a few lines of script) before content builds","When exporting from spreadsheets, check for auto-generated duplicate columns like \"Column1, Column1\""],"tags":["hugo","csv","unmarshal","data-validation"],"analyzedSha":"8a468df065a75c1c7cf9f6850f32148746590ea5","analyzedAt":"2026-07-31T21:23:07.045Z","schemaVersion":2}