Ruby Logo

CSV, JSON & YAML Processing

Learn to parse and generate structured data formats including CSV, JSON, and YAML with practical examples.

Home Ruby CSV, JSON & YAML Processing

CSV, JSON & YAML Processing

Master Ruby's powerful data format processing capabilities. Learn to parse and generate CSV files, work with JSON data, and handle YAML configuration files efficiently. Essential skills for data processing, API integration, and configuration management.

Understanding Data Formats

CSV

Comma-Separated Values. Simple tabular data, Excel-compatible, perfect for data exports.

JSON

JavaScript Object Notation. Web APIs, configuration, lightweight data exchange.

YAML

YAML Ain't Markup Language. Human-readable configuration files, documentation.

Common Use Cases

  • CSV: Data imports/exports, spreadsheet processing, reports
  • JSON: REST APIs, web applications, data storage
  • YAML: Configuration files, documentation, data serialization

CSV Processing with Ruby

Reading CSV Files

require 'csv'

# Simple CSV reading
CSV.foreach("employees.csv") do |row|
  puts "Employee: #{row[0]}, Position: #{row[1]}, Salary: #{row[2]}"
end

# Reading with headers
CSV.foreach("employees.csv", headers: true) do |row|
  puts "Employee: #{row['name']}, Position: #{row['position']}"
  puts "Salary: #{row['salary']}"
end

# Reading all data at once
data = CSV.read("employees.csv")
data.each do |row|
  puts row.join(" | ")
end

# Reading with headers into array of hashes
employees = CSV.read("employees.csv", headers: true, header_converters: :symbol)
employees.each do |employee|
  puts "#{employee[:name]} works as #{employee[:position]}"
  puts "Salary: $#{employee[:salary]}"
end

# Custom field separator
CSV.foreach("data.txt", col_sep: "\t") do |row|  # Tab-separated
  puts row.inspect
end

# With type conversion
CSV.foreach("numbers.csv", converters: :numeric) do |row|
  puts "Sum: #{row.sum}"  # Automatically converts to numbers
end

Writing CSV Files

# Writing CSV data
CSV.open("output.csv", "w") do |csv|
  csv << ["Name", "Age", "City"]     # Header row
  csv << ["Alice", 30, "New York"]
  csv << ["Bob", 25, "Los Angeles"]
  csv << ["Carol", 35, "Chicago"]
end

# Writing from array of arrays
data = [
  ["Name", "Score", "Grade"],
  ["Alice", 95, "A"],
  ["Bob", 87, "B"],
  ["Carol", 92, "A"]
]

CSV.open("grades.csv", "w") do |csv|
  data.each { |row| csv << row }
end

# Writing from array of hashes
students = [
  { name: "Alice", age: 20, major: "Computer Science" },
  { name: "Bob", age: 22, major: "Mathematics" },
  { name: "Carol", age: 21, major: "Physics" }
]

CSV.open("students.csv", "w") do |csv|
  # Write headers
  csv << students.first.keys

  # Write data
  students.each do |student|
    csv << student.values
  end
end

# Generate CSV string in memory
csv_string = CSV.generate do |csv|
  csv << ["Product", "Price", "Quantity"]
  csv << ["Laptop", 999.99, 10]
  csv << ["Mouse", 25.99, 50]
end

puts csv_string

CSV Options and Converters

headers: true
First row contains column names
col_sep: ","
Field separator (comma, tab, pipe, etc.)
quote_char: "\""
Character for quoting fields
converters: :numeric
Convert strings to numbers
header_converters: :symbol
Convert headers to symbols
skip_blanks: true
Skip empty rows

JSON Processing

Parsing JSON Data

require 'json'

# Parse JSON string
json_string = '{"name": "Alice", "age": 30, "city": "New York"}'
data = JSON.parse(json_string)
puts data["name"]    # "Alice"
puts data["age"]     # 30

# Parse JSON with symbol keys
data_symbols = JSON.parse(json_string, symbolize_names: true)
puts data_symbols[:name]    # "Alice"
puts data_symbols[:age]     # 30

# Parse JSON from file
data = JSON.parse(File.read("config.json"))

# Safe parsing with error handling
def safe_parse_json(json_string)
  begin
    JSON.parse(json_string)
  rescue JSON::ParserError => e
    puts "Invalid JSON: #{e.message}"
    nil
  end
end

# Complex nested JSON
complex_json = '{
  "users": [
    {"id": 1, "name": "Alice", "email": "alice@example.com"},
    {"id": 2, "name": "Bob", "email": "bob@example.com"}
  ],
  "metadata": {
    "total": 2,
    "page": 1
  }
}'

data = JSON.parse(complex_json, symbolize_names: true)
data[:users].each do |user|
  puts "User #{user[:id]}: #{user[:name]} (#{user[:email]})"
end
puts "Total users: #{data[:metadata][:total]}"

Generating JSON Data

# Generate JSON from Ruby objects
user = {
  name: "Alice",
  age: 30,
  email: "alice@example.com",
  active: true
}

json_output = JSON.generate(user)
puts json_output
# {"name":"Alice","age":30,"email":"alice@example.com","active":true}

# Pretty printing JSON
puts JSON.pretty_generate(user)
# {
#   "name": "Alice",
#   "age": 30,
#   "email": "alice@example.com",
#   "active": true
# }

# Convert arrays to JSON
users = [
  { name: "Alice", age: 30 },
  { name: "Bob", age: 25 },
  { name: "Carol", age: 35 }
]

json_users = JSON.pretty_generate(users)
puts json_users

# Write JSON to file
File.write("users.json", JSON.pretty_generate(users))

# Using to_json method (available on most objects)
puts user.to_json

# Custom JSON serialization
class Person
  attr_accessor :name, :age, :email

  def initialize(name, age, email)
    @name = name
    @age = age
    @email = email
  end

  def to_json(*args)
    {
      name: @name,
      age: @age,
      email: @email,
      type: "Person"
    }.to_json(*args)
  end
end

person = Person.new("Alice", 30, "alice@example.com")
puts person.to_json

JSON Best Practices

  • Error Handling: Always rescue JSON::ParserError when parsing
  • Symbol Keys: Use symbolize_names: true for cleaner access
  • Pretty Print: Use JSON.pretty_generate for readable output
  • File Operations: Combine with File.read/write for persistence
  • API Responses: Perfect for REST API data exchange

YAML Processing

Loading YAML Data

require 'yaml'

# Parse YAML string
yaml_string = <<~YAML
  name: Alice
  age: 30
  city: New York
  skills:
    - Ruby
    - JavaScript
    - Python
  active: true
YAML

data = YAML.load(yaml_string)
puts data["name"]           # "Alice"
puts data["skills"]         # ["Ruby", "JavaScript", "Python"]

# Load from file
config = YAML.load_file("config.yml")

# Safe loading (recommended for untrusted input)
data = YAML.safe_load(yaml_string)

# Load multiple documents from one file
documents = YAML.load_stream(File.read("multi_doc.yml"))
documents.each { |doc| puts doc.inspect }

# Complex YAML configuration
config_yaml = <<~YAML
  database:
    adapter: postgresql
    host: localhost
    port: 5432
    database: myapp_production
    username: myapp
    password: secret123

  redis:
    url: redis://localhost:6379/0

  logging:
    level: info
    file: /var/log/myapp.log

  features:
    email_notifications: true
    two_factor_auth: false
    beta_features:
      - advanced_search
      - real_time_updates
YAML

config = YAML.safe_load(config_yaml)
puts "Database: #{config['database']['adapter']}"
puts "Redis URL: #{config['redis']['url']}"
puts "Log Level: #{config['logging']['level']}"
puts "Features: #{config['features']['beta_features'].join(', ')}"

Generating YAML Data

# Generate YAML from Ruby objects
user = {
  "name" => "Alice",
  "age" => 30,
  "email" => "alice@example.com",
  "skills" => ["Ruby", "Rails", "JavaScript"],
  "address" => {
    "street" => "123 Main St",
    "city" => "New York",
    "zip" => "10001"
  }
}

yaml_output = YAML.dump(user)
puts yaml_output

# Write YAML to file
File.write("user_config.yml", YAML.dump(user))

# Generate YAML for multiple objects
users = [
  { name: "Alice", age: 30, role: "developer" },
  { name: "Bob", age: 25, role: "designer" },
  { name: "Carol", age: 35, role: "manager" }
]

File.write("users.yml", YAML.dump(users))

# Custom YAML serialization
class DatabaseConfig
  attr_accessor :host, :port, :database, :username

  def initialize(host, port, database, username)
    @host = host
    @port = port
    @database = database
    @username = username
  end

  def to_yaml
    {
      "host" => @host,
      "port" => @port,
      "database" => @database,
      "username" => @username
    }.to_yaml
  end
end

db_config = DatabaseConfig.new("localhost", 5432, "myapp", "user")
puts db_config.to_yaml

# Environment-specific configurations
environments = {
  "development" => {
    "database" => {
      "adapter" => "sqlite3",
      "database" => "db/development.sqlite3"
    }
  },
  "production" => {
    "database" => {
      "adapter" => "postgresql",
      "host" => "localhost",
      "database" => "myapp_production"
    }
  }
}

File.write("database.yml", YAML.dump(environments))

YAML Safety and Best Practices

  • Use YAML.safe_load: Prevents code execution for untrusted input
  • Symbolic Keys: YAML naturally works well with string keys
  • Indentation: YAML is whitespace-sensitive, use consistent spaces
  • Comments: YAML supports comments with # character
  • Multi-line Strings: Use | for literal blocks, > for folded
  • Environment Configs: Perfect for Rails-style configuration

Practical Data Processing Examples

Sales Data Analyzer

require 'csv'
require 'json'

class SalesAnalyzer
  def initialize(csv_file)
    @csv_file = csv_file
    @sales_data = []
    load_data
  end

  def load_data
    CSV.foreach(@csv_file, headers: true, header_converters: :symbol) do |row|
      @sales_data << {
        date: Date.parse(row[:date]),
        product: row[:product],
        amount: row[:amount].to_f,
        quantity: row[:quantity].to_i,
        region: row[:region]
      }
    end
  end

  def total_sales
    @sales_data.sum { |sale| sale[:amount] }
  end

  def sales_by_region
    @sales_data.group_by { |sale| sale[:region] }
               .transform_values { |sales| sales.sum { |s| s[:amount] } }
  end

  def top_products(limit = 5)
    @sales_data.group_by { |sale| sale[:product] }
               .transform_values { |sales| sales.sum { |s| s[:amount] } }
               .sort_by { |_, amount| -amount }
               .first(limit)
               .to_h
  end

  def export_summary(filename)
    summary = {
      total_sales: total_sales,
      sales_by_region: sales_by_region,
      top_products: top_products,
      generated_at: Time.now.iso8601
    }

    File.write(filename, JSON.pretty_generate(summary))
  end

  def export_regional_csv(region, filename)
    regional_sales = @sales_data.select { |sale| sale[:region] == region }

    CSV.open(filename, "w") do |csv|
      csv << ["Date", "Product", "Amount", "Quantity"]
      regional_sales.each do |sale|
        csv << [sale[:date], sale[:product], sale[:amount], sale[:quantity]]
      end
    end
  end
end

# Usage
analyzer = SalesAnalyzer.new("sales_data.csv")
puts "Total Sales: $#{analyzer.total_sales}"
analyzer.export_summary("sales_summary.json")
analyzer.export_regional_csv("North", "north_sales.csv")

Configuration Manager

require 'yaml'
require 'json'

class ConfigManager
  def initialize(config_file)
    @config_file = config_file
    @config = load_config
  end

  def get(key_path)
    keys = key_path.split('.')
    keys.reduce(@config) { |config, key| config&.dig(key) }
  end

  def set(key_path, value)
    keys = key_path.split('.')
    target = keys[0..-2].reduce(@config) { |config, key| config[key] ||= {} }
    target[keys.last] = value
    save_config
  end

  def export_json(filename)
    File.write(filename, JSON.pretty_generate(@config))
  end

  def merge_environment(env_file)
    env_config = YAML.safe_load_file(env_file)
    @config = deep_merge(@config, env_config)
    save_config
  end

  private

  def load_config
    if File.exist?(@config_file)
      case File.extname(@config_file)
      when '.yml', '.yaml'
        YAML.safe_load_file(@config_file) || {}
      when '.json'
        JSON.parse(File.read(@config_file))
      else
        {}
      end
    else
      {}
    end
  end

  def save_config
    case File.extname(@config_file)
    when '.yml', '.yaml'
      File.write(@config_file, YAML.dump(@config))
    when '.json'
      File.write(@config_file, JSON.pretty_generate(@config))
    end
  end

  def deep_merge(hash1, hash2)
    hash1.merge(hash2) do |key, old_val, new_val|
      if old_val.is_a?(Hash) && new_val.is_a?(Hash)
        deep_merge(old_val, new_val)
      else
        new_val
      end
    end
  end
end

# Usage
config = ConfigManager.new("app_config.yml")
config.set("database.host", "localhost")
config.set("database.port", 5432)
config.set("features.notifications", true)

puts config.get("database.host")  # "localhost"
config.export_json("config_backup.json")

When to Use Each Format

Use CSV When:

  • Dealing with tabular data
  • Excel compatibility needed
  • Simple data structure
  • Large datasets efficiently
  • Data analysis/reporting

Use JSON When:

  • Building web APIs
  • JavaScript integration
  • Nested data structures
  • Real-time data exchange
  • NoSQL databases

Use YAML When:

  • Configuration files
  • Human readability matters
  • Comments needed
  • Multi-line strings
  • DevOps and deployment

Quick Navigation

Related Topics

Video Tutorial

Watch and learn csv, json & yaml processing

Pro Tip: After reading through the content above, watch this video to reinforce your understanding and see the concepts in action!