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
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