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)
- csvpputil - JSON/YAML conversion utilities
- csvpp CLI - Command-line tool for viewing and converting CSV++ files
- Go 1.26 or later
GOEXPERIMENT=jsonv2environment variable (required bycsvpputilpackage, which usesencoding/json/jsontext)
go get github.com/osamingo/go-csvppWhen building or testing packages that depend on csvpputil, set the experiment flag:
GOEXPERIMENT=jsonv2 go build ./...
GOEXPERIMENT=jsonv2 go test ./...package 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 (r is io.Reader)
var people []Person
err := csvpp.Unmarshal(r, &people)
// Marshal structs to CSV++ data (w is io.Writer)
err := csvpp.Marshal(w, people)See Struct Mapping above for tag syntax and usage.
Utility package for converting CSV++ data to JSON and YAML formats with streaming support.
For details, see csvpputil/README.md.
A command-line tool for viewing, converting, and validating CSV++ files.
# Install
go install github.com/osamingo/go-csvpp/cmd/csvpp@latest
# Validate
csvpp validate input.csvpp
# Convert to JSON/YAML
csvpp convert -i input.csvpp -o output.json
csvpp convert -i input.csvpp -o output.yaml
# Interactive TUI view
csvpp view input.csvppFor more details, see cmd/csvpp/README.md.
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.