A Go implementation of the IETF CSV++ specification (draft-mscaldas-csvpp-01).
CSV++ extends traditional CSV to support arrays and structured fields within cells, enabling complex data representation while maintaining CSV's simplicity.
- Full IETF CSV++ specification compliance
- Wraps
encoding/csvfor RFC 4180 compatibility - Four field types: Simple, Array, Structured, ArrayStructured
- Struct mapping with
csvpptags (Marshal/Unmarshal) - Configurable delimiters
- Security-conscious design (nesting depth limits)
- Go 1.24 or later
go get github.com/osamingo/go-csvpppackage main
import (
"fmt"
"io"
"strings"
"github.com/osamingo/go-csvpp"
)
func main() {
input := `name,phone[],geo(lat^lon)
Alice,555-1234~555-5678,34.0522^-118.2437
Bob,555-9999,40.7128^-74.0060
`
reader := csvpp.NewReader(strings.NewReader(input))
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
panic(err)
}
name := record[0].Value
phones := record[1].Values
lat := record[2].Components[0].Value
lon := record[2].Components[1].Value
fmt.Printf("%s: phones=%v, location=(%s, %s)\n", name, phones, lat, lon)
}
}Output:
Alice: phones=[555-1234 555-5678], location=(34.0522, -118.2437)
Bob: phones=[555-9999], location=(40.7128, -74.0060)
package main
import (
"bytes"
"fmt"
"github.com/osamingo/go-csvpp"
)
func main() {
var buf bytes.Buffer
writer := csvpp.NewWriter(&buf)
headers := []*csvpp.ColumnHeader{
{Name: "name", Kind: csvpp.SimpleField},
{Name: "tags", Kind: csvpp.ArrayField, ArrayDelimiter: '~'},
}
writer.SetHeaders(headers)
if err := writer.WriteHeader(); err != nil {
panic(err)
}
if err := writer.Write([]*csvpp.Field{
{Value: "Alice"},
{Values: []string{"go", "rust", "python"}},
}); err != nil {
panic(err)
}
writer.Flush()
fmt.Print(buf.String())
}Output:
name,tags[]
Alice,go~rust~python
package main
import (
"fmt"
"strings"
"github.com/osamingo/go-csvpp"
)
type Person struct {
Name string `csvpp:"name"`
Phones []string `csvpp:"phone[]"`
Geo struct {
Lat string
Lon string
} `csvpp:"geo(lat^lon)"`
}
func main() {
input := `name,phone[],geo(lat^lon)
Alice,555-1234~555-5678,34.0522^-118.2437
`
var people []Person
if err := csvpp.Unmarshal(strings.NewReader(input), &people); err != nil {
panic(err)
}
for _, p := range people {
fmt.Printf("%s: phones=%v, geo=(%s, %s)\n",
p.Name, p.Phones, p.Geo.Lat, p.Geo.Lon)
}
}Output:
Alice: phones=[555-1234 555-5678], geo=(34.0522, -118.2437)
CSV++ supports four field types in headers:
| Type | Header Syntax | Example Data | Description |
|---|---|---|---|
| Simple | name |
Alice |
Plain text value |
| Array | tags[] |
go~rust~python |
Multiple values with delimiter |
| Structured | geo(lat^lon) |
34.05^-118.24 |
Named components |
| ArrayStructured | addr[](city^zip) |
LA^90210~NY^10001 |
Array of structures |
- Array delimiter:
~(tilde) - Component delimiter:
^(caret)
Custom delimiters can be specified in the header:
phone[|]- uses|as array delimitergeo;(lat;lon)- uses;as component delimiter
For nested structures, the IETF specification recommends:
| Level | Delimiter |
|---|---|
| 1 (arrays) | ~ |
| 2 (components) | ^ |
| 3 | ; |
| 4 | : |
reader := csvpp.NewReader(r) // r is io.Reader
// Configuration (same as encoding/csv)
reader.Comma = ',' // Field delimiter
reader.Comment = '#' // Comment character
reader.LazyQuotes = false // Relaxed quote handling
reader.TrimLeadingSpace = false
reader.MaxNestingDepth = 10 // Nesting limit (security)
// Methods
headers, err := reader.Headers() // Get parsed headers
record, err := reader.Read() // Read one record
records, err := reader.ReadAll() // Read all recordswriter := csvpp.NewWriter(w) // w is io.Writer
// Configuration
writer.Comma = ',' // Field delimiter
writer.UseCRLF = false // Use \r\n line endings
// Methods
writer.SetHeaders(headers) // Set column headers
writer.WriteHeader() // Write header row
writer.Write(record) // Write one record
writer.WriteAll(records) // Write all records
writer.Flush() // Flush buffer// Unmarshal CSV++ data into structs
var people []Person
err := csvpp.Unmarshal(reader, &people)
// Marshal structs to CSV++ data
err := csvpp.Marshal(writer, people)Use csvpp struct tags to map fields:
type Record struct {
Name string `csvpp:"name"` // Simple field
Tags []string `csvpp:"tags[]"` // Array field
Location struct { // Structured field
Lat string
Lon string
} `csvpp:"geo(lat^lon)"`
Addresses []Address `csvpp:"addr[](street^city)"` // Array structured
}This package wraps encoding/csv and inherits:
- Full RFC 4180 compliance
- Quoted field handling
- Configurable field/line delimiters
- Comment support
- MaxNestingDepth: Limits nested structure depth (default: 10) to prevent stack overflow from malicious input
- Header names are restricted to ASCII characters per IETF specification
When CSV files are opened in spreadsheet applications, values starting with =, +, -, or @ may be interpreted as formulas. Use HasFormulaPrefix to detect and escape dangerous values:
if csvpp.HasFormulaPrefix(value) {
value = "'" + value // Escape for spreadsheet safety
}This implementation follows the IETF CSV++ specification:
MIT License - see LICENSE for details.
Contributions are welcome! Please feel free to submit a Pull Request.