main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
package main

import (
	"encoding/csv"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"strings"
)

// CSVtoJSONConverter handles streaming CSV to JSON conversion
type CSVtoJSONConverter struct {
	reader       *csv.Reader
	writer       *json.Encoder
	headers      []string
	delimiter    rune
	trimWhitespace bool
}

// NewCSVtoJSONConverter creates a new converter from input and output io.Reader/io.Writer
func NewCSVtoJSONConverter(input io.Reader, output io.Writer, delimiter rune, trimWhitespace bool) *CSVtoJSONConverter {
	reader := csv.NewReader(input)
	reader.Comma = delimiter
	reader.TrimLeadingSpace = trimWhitespace

	return &CSVtoJSONConverter{
		reader:       reader,
		writer:       json.NewEncoder(output),
		delimiter:    delimiter,
		trimWhitespace: trimWhitespace,
	}
}

// Convert processes the CSV file and writes JSON output line-by-line
func (c *CSVtoJSONConverter) Convert() error {
	// Read CSV headers
	headers, err := c.reader.Read()
	if err != nil {
		return fmt.Errorf("failed to read headers: %w", err)
	}
	c.headers = headers

	// Write JSON array start
	if _, err := fmt.Fprint(c.writer.Writer, "["); err != nil {
		return fmt.Errorf("failed to write JSON start: %w", err)
	}

	firstRow := true
	for {
		// Read CSV row
		row, err := c.reader.Read()
		if err == io.EOF {
			break
		}
		if err != nil {
			return fmt.Errorf("failed to read row: %w", err)
		}

		// Skip empty rows
		if len(row) == 0 {
			continue
		}

		// Map row to JSON object
		jsonRow, err := c.rowToJSON(row)
		if err != nil {
			return fmt.Errorf("failed to convert row: %w", err)
		}

		// Add comma separator between rows
		if !firstRow {
			if _, err := fmt.Fprint(c.writer.Writer, ",\n"); err != nil {
				return err
			}
		} else {
			firstRow = false
		}

		// Encode JSON object
		if err := c.writer.Encode(jsonRow); err != nil {
			return fmt.Errorf("failed to encode JSON: %w", err)
		}
	}

	// Write JSON array end
	if _, err := fmt.Fprint(c.writer.Writer, "]\n"); err != nil {
		return fmt.Errorf("failed to write JSON end: %w", err)
	}

	return nil
}

// rowToJSON converts a single CSV row to a JSON object
func (c *CSVtoJSONConverter) rowToJSON(row []string) (map[string]string, error) {
	jsonRow := make(map[string]string)

	// Use header for keys; if row is shorter than headers, fill with empty string
	for i, header := range c.headers {
		if i < len(row) {
			value := row[i]
			if c.trimWhitespace {
				value = strings.TrimSpace(value)
			}
			jsonRow[header] = value
		} else {
			jsonRow[header] = ""
		}
	}

	// If row is longer than headers, add with index keys
	for i := len(c.headers); i < len(row); i++ {
		key := fmt.Sprintf("column_%d", i+1)
		value := row[i]
		if c.trimWhitespace {
			value = strings.TrimSpace(value)
		}
		jsonRow[key] = value
	}

	return jsonRow, nil
}

func main() {
	// Check command line arguments
	if len(os.Args) < 3 {
		fmt.Printf("Usage: %s <input.csv> <output.json> [delimiter]\n", os.Args[0])
		os.Exit(1)
	}

	inputPath := os.Args[1]
	outputPath := os.Args[2]
	delimiter := ','
	if len(os.Args) >= 4 && len(os.Args[3]) > 0 {
		delimiter = rune(os.Args[3][0])
	}

	// Open input file
	inputFile, err := os.Open(inputPath)
	if err != nil {
		fmt.Printf("Failed to open input file: %v\n", err)
		os.Exit(1)
	}
	defer inputFile.Close()

	// Create output file
	outputFile, err := os.Create(outputPath)
	if err != nil {
		fmt.Printf("Failed to create output file: %v\n", err)
		os.Exit(1)
	}
	defer outputFile.Close()

	// Create converter
	converter := NewCSVtoJSONConverter(inputFile, outputFile, delimiter, true)

	// Run conversion
	if err := converter.Convert(); err != nil {
		fmt.Printf("Conversion failed: %v\n", err)
		os.Exit(1)
	}

	fmt.Printf("Successfully converted %s to %s\n", inputPath, outputPath)
}

How It Works

Streams a CSV file line-by-line and emits JSON records without loading the entire file, supporting custom delimiters and headers.

Opens a CSV reader with configurable comma, reads rows iteratively, maps columns to keys, encodes each record to JSON, and writes to output while handling errors and end-of-file cleanly.

Key Concepts

  • 1Streaming approach keeps memory constant for huge files.
  • 2Header mapping controls field names and order.
  • 3Error handling captures malformed rows without crashing the pipeline.

When to Use This Pattern

  • Converting large exports to JSON for ingestion.
  • Feeding APIs or message queues from CSV data.
  • CLI utilities for quick data format transformations.

Best Practices

  • Validate header counts versus row lengths.
  • Handle encoding and escaping properly in JSON output.
  • Close files and flush writers to avoid partial output.
Go Version1.18
Difficultyintermediate
Production ReadyYes
Lines of Code164