Ruby JSON
JSON (JavaScript Object Notation) is a lightweight data exchange format widely used in Web services, API communication, and configuration files. Ruby provides a built-in JSON library that makes parsing and generating JSON data simple and intuitive. This chapter will explain in detail how to handle JSON data in Ruby, including parsing, generating, validation, and best practices.
🎯 JSON Basics
What is JSON
JSON (JavaScript Object Notation) is a text-based open standard data exchange format. It has the following characteristics:
- Lightweight: More concise than XML
- Human-readable: Text format readable by humans
- Language-independent: Supports multiple programming languages
- Structured: Supports complex data structures like objects and arrays
JSON Syntax
json
{
"name": "John",
"age": 25,
"isStudent": true,
"courses": ["Math", "English", "Computer Science"],
"address": {
"city": "New York",
"district": "Manhattan"
},
"score": null
}📖 JSON Parsing
Basic Parsing
ruby
require 'json'
# Parse JSON string
json_string = '{"name": "John", "age": 25, "city": "Beijing"}'
parsed_data = JSON.parse(json_string)
puts parsed_data['name'] # John
puts parsed_data['age'] # 25
puts parsed_data['city'] # Beijing
# Parse JSON array
json_array = '[1, 2, 3, 4, 5]'
numbers = JSON.parse(json_array)
puts numbers.inspect # [1, 2, 3, 4, 5]
# Parse complex JSON structure
complex_json = <<~JSON
{
"users": [
{
"id": 1,
"name": "John",
"email": "john@example.com",
"profile": {
"age": 25,
"city": "Beijing"
}
},
{
"id": 2,
"name": "Jane",
"email": "jane@example.com",
"profile": {
"age": 30,
"city": "Shanghai"
}
}
],
"total": 2
}
JSON
data = JSON.parse(complex_json)
puts "Total users: #{data['total']}"
puts "First user: #{data['users'][0]['name']}"
puts "Second user age: #{data['users'][1]['profile']['age']}"Symbolized Keys Parsing
ruby
require 'json'
# Using symbols as keys
json_string = '{"name": "John", "age": 25, "city": "Beijing"}'
# Default string keys
default_parsed = JSON.parse(json_string)
puts default_parsed.keys # ["name", "age", "city"]
# Using symbol keys
symbolized_parsed = JSON.parse(json_string, symbolize_names: true)
puts symbolized_parsed.keys # [:name, :age, :city]
puts symbolized_parsed[:name] # John
# Create custom objects
class Person
attr_accessor :name, :age, :city
def initialize(name, age, city)
@name = name
@age = age
@city = city
end
def self.from_json(json_string)
data = JSON.parse(json_string, symbolize_names: true)
new(data[:name], data[:age], data[:city])
end
end
# Using custom objects
person_json = '{"name": "Mike", "age": 28, "city": "Guangzhou"}'
person = Person.from_json(person_json)
puts "Name: #{person.name}, Age: #{person.age}, City: #{person.city}"Error Handling
ruby
require 'json'
# Safely parse JSON
def safe_json_parse(json_string)
begin
JSON.parse(json_string)
rescue JSON::ParserError => e
puts "JSON parsing error: #{e.message}"
nil
end
end
# Valid JSON
valid_json = '{"name": "John", "age": 25}'
result = safe_json_parse(valid_json)
puts result.inspect unless result.nil?
# Invalid JSON
invalid_json = '{"name": "John", "age": 25' # Missing closing brace
result = safe_json_parse(invalid_json)
puts "Parsing result: #{result}" # Parsing result:
# Handling special characters
json_with_unicode = '{"message": "Hello, World!"}'
parsed = JSON.parse(json_with_unicode)
puts parsed['message'] # Hello, World!🎨 JSON Generation
Basic Generation
ruby
require 'json'
# Generate JSON from hash
data = {
name: "John",
age: 25,
city: "Beijing",
hobbies: ["Reading", "Swimming", "Programming"]
}
json_string = JSON.generate(data)
puts json_string
# {"name":"John","age":25,"city":"Beijing","hobbies":["Reading","Swimming","Programming"]}
# Pretty printing
pretty_json = JSON.pretty_generate(data)
puts pretty_json
# {
# "name": "John",
# "age": 25,
# "city": "Beijing",
# "hobbies": [
# "Reading",
# "Swimming",
# "Programming"
# ]
# }
# Generate JSON from array
numbers = [1, 2, 3, 4, 5]
json_array = JSON.generate(numbers)
puts json_array # [1,2,3,4,5]Custom Object Serialization
ruby
require 'json'
# Add JSON serialization support for custom class
class User
attr_accessor :id, :name, :email, :created_at
def initialize(id, name, email)
@id = id
@name = name
@email = email
@created_at = Time.now
end
# Define how to convert to JSON
def to_json(*args)
{
id: @id,
name: @name,
email: @email,
created_at: @created_at.iso8601
}.to_json(*args)
end
# Create object from JSON
def self.from_json(json_string)
data = JSON.parse(json_string, symbolize_names: true)
user = new(data[:id], data[:name], data[:email])
user.created_at = Time.parse(data[:created_at])
user
end
end
# Using custom serialization
user = User.new(1, "John", "john@example.com")
json_output = user.to_json
puts json_output
# Restore object from JSON
restored_user = User.from_json(json_output)
puts "Restored user: #{restored_user.name}, Created at: #{restored_user.created_at}"Controlling JSON Output Format
ruby
require 'json'
# Control JSON generation options
data = {
name: "John",
age: 25,
bio: "This is a very long personal bio with lots of information.",
scores: [95, 87, 92, 88, 90]
}
# Standard generation
puts "=== Standard Generation ==="
puts JSON.generate(data)
# Pretty generation
puts "\n=== Pretty Generation ==="
puts JSON.pretty_generate(data)
# Custom indentation
puts "\n=== Custom Indentation ==="
puts JSON.pretty_generate(data, indent: ' ')
# Limit depth
deep_data = {
level1: {
level2: {
level3: {
level4: "Deep data"
}
}
}
}
puts "\n=== Limit Depth ==="
puts JSON.pretty_generate(deep_data, max_nesting: 3)
# ASCII encoding (avoid Unicode escapes)
chinese_data = {
name: "John",
city: "Beijing"
}
puts "\n=== ASCII Encoding ==="
puts JSON.generate(chinese_data, ascii_only: true)
puts "\n=== UTF-8 Encoding ==="
puts JSON.generate(chinese_data)🔄 JSON and Other Data Formats
JSON to YAML Conversion
ruby
require 'json'
require 'yaml'
# JSON to YAML
json_data = '{"name": "John", "age": 25, "hobbies": ["Reading", "Swimming"]}'
parsed_data = JSON.parse(json_data)
yaml_output = YAML.dump(parsed_data)
puts "YAML format:"
puts yaml_output
# YAML to JSON
yaml_data = <<~YAML
---
name: Jane
age: 30
hobbies:
- Travel
- Photography
YAML
yaml_parsed = YAML.load(yaml_data)
json_output = JSON.generate(yaml_parsed)
puts "\nJSON format:"
puts json_outputJSON to CSV Conversion
ruby
require 'json'
require 'csv'
# JSON array to CSV
json_array = <<~JSON
[
{"name": "John", "age": 25, "city": "Beijing"},
{"name": "Jane", "age": 30, "city": "Shanghai"},
{"name": "Mike", "age": 28, "city": "Guangzhou"}
]
JSON
data = JSON.parse(json_array)
# Generate CSV
csv_string = CSV.generate do |csv|
# Write header row
csv << data.first.keys
# Write data rows
data.each do |row|
csv << row.values
end
end
puts "CSV format:"
puts csv_string
# CSV to JSON
csv_data = <<~CSV
name,age,city
John,25,Beijing
Jane,30,Shanghai
Mike,28,Guangzhou
CSV
csv_parsed = CSV.parse(csv_data, headers: true)
json_output = JSON.pretty_generate(csv_parsed.map(&:to_hash))
puts "\nJSON format:"
puts json_output🎯 JSON Practical Examples
API Client Handling
ruby
require 'json'
require 'net/http'
require 'uri'
class APIClient
def initialize(base_url)
@base_url = base_url
end
def get(path)
uri = URI.join(@base_url, path)
response = Net::HTTP.get_response(uri)
if response.code == '200'
JSON.parse(response.body)
else
{ error: "HTTP #{response.code}", message: response.message }
end
end
def post(path, data)
uri = URI.join(@base_url, path)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request.body = JSON.generate(data)
response = http.request(request)
if response.code.start_with?('2')
JSON.parse(response.body)
else
{ error: "HTTP #{response.code}", message: response.message }
end
end
end
# Using API client
# client = APIClient.new('https://api.example.com/')
#
# # GET request
# users = client.get('/api/users')
# puts JSON.pretty_generate(users)
#
# # POST request
# new_user = client.post('/api/users', { name: 'New User', email: 'new@example.com' })
# puts JSON.pretty_generate(new_user)Configuration File Handling
ruby
require 'json'
class ConfigManager
def initialize(config_file)
@config_file = config_file
@config = load_config
end
def [](key)
@config[key]
end
def []=(key, value)
@config[key] = value
save_config
end
def get(key, default = nil)
@config.fetch(key, default)
end
def set(key, value)
@config[key] = value
save_config
end
def reload
@config = load_config
end
private
def load_config
if File.exist?(@config_file)
JSON.parse(File.read(@config_file))
else
{}
end
rescue JSON::ParserError => e
puts "Configuration file parsing error: #{e.message}"
{}
end
def save_config
File.write(@config_file, JSON.pretty_generate(@config))
end
end
# Using config manager
# Create sample configuration file
sample_config = {
"database" => {
"host" => "localhost",
"port" => 5432,
"name" => "myapp_db"
},
"logging" => {
"level" => "INFO",
"file" => "/var/log/myapp.log"
},
"features" => {
"user_management" => true,
"reporting" => false
}
}
File.write('app_config.json', JSON.pretty_generate(sample_config))
# Using config manager
config = ConfigManager.new('app_config.json')
puts "Database host: #{config['database']['host']}"
puts "Log level: #{config.get('logging', {})['level']}"
puts "User management feature: #{config.get('features', {})['user_management']}"
# Update configuration
config.set('features.user_management', false)
config['logging']['level'] = 'DEBUG'
# Reload configuration
config.reload
puts "Updated log level: #{config['logging']['level']}"Data Validation and Sanitization
ruby
require 'json'
class JSONValidator
# Validate JSON structure
def self.validate_structure(data, schema)
case schema
when Hash
validate_hash(data, schema)
when Array
validate_array(data, schema)
else
data.is_a?(schema)
end
end
# Sanitize JSON data
def self.sanitize(data)
case data
when Hash
data.each_with_object({}) do |(key, value), result|
result[key.to_s] = sanitize(value)
end
when Array
data.map { |item| sanitize(item) }
when String
data.strip
else
data
end
end
# Deep merge JSON objects
def self.deep_merge(hash1, hash2)
merged = hash1.dup
hash2.each do |key, value|
merged[key] = if merged[key].is_a?(Hash) && value.is_a?(Hash)
deep_merge(merged[key], value)
else
value
end
end
merged
end
private
def self.validate_hash(data, schema)
return false unless data.is_a?(Hash)
schema.all? do |key, value_schema|
if key.to_s.end_with?('?')
# Optional field
optional_key = key.to_s.chomp('?').to_sym
!data.key?(optional_key) || validate_structure(data[optional_key], value_schema)
else
# Required field
data.key?(key) && validate_structure(data[key], value_schema)
end
end
end
def self.validate_array(data, schema)
return false unless data.is_a?(Array)
return true if schema.empty?
item_schema = schema.first
data.all? { |item| validate_structure(item, item_schema) }
end
end
# Using validator
user_schema = {
name: String,
age: Integer,
email: String,
"address?" => {
street: String,
city: String
}
}
valid_user = {
name: "John",
age: 25,
email: "john@example.com",
address: {
street: "123 Main St",
city: "Beijing"
}
}
invalid_user = {
name: "Jane",
age: "not a number",
email: "jane@example.com"
}
puts "Valid user validation: #{JSONValidator.validate_structure(valid_user, user_schema)}"
puts "Invalid user validation: #{JSONValidator.validate_structure(invalid_user, user_schema)}"
# Using data sanitization
dirty_data = {
name: " John ",
age: 25,
hobbies: [" Reading ", " Swimming ", " Programming "],
address: {
city: " Beijing "
}
}
clean_data = JSONValidator.sanitize(dirty_data)
puts "Sanitized data:"
puts JSON.pretty_generate(clean_data)
# Deep merge
default_config = {
database: {
host: "localhost",
port: 5432,
pool: 5
},
logging: {
level: "INFO",
format: "json"
}
}
user_config = {
database: {
host: "production.db.com",
pool: 10
},
features: {
caching: true
}
}
merged_config = JSONValidator.deep_merge(default_config, user_config)
puts "Merged configuration:"
puts JSON.pretty_generate(merged_config)📊 JSON Performance Optimization
Large JSON Processing
ruby
require 'json'
require 'stringio'
# Streaming JSON parsing (for large files)
class StreamJSONParser
def self.parse_large_file(file_path)
File.open(file_path, 'r') do |file|
# For very large JSON files, consider using streaming parsing libraries like yajl-ruby
# Here demonstrating basic concepts
json_string = file.read
JSON.parse(json_string)
end
end
# Chunk processing of JSON array
def self.process_json_array_chunks(json_array, chunk_size = 1000)
json_array.each_slice(chunk_size) do |chunk|
yield chunk
end
end
end
# Create large JSON data for testing
large_data = {
users: (1..10000).map do |i|
{
id: i,
name: "User#{i}",
email: "user#{i}@example.com",
score: rand(100)
}
end
}
# Write to file
File.write('large_data.json', JSON.generate(large_data))
# Chunk processing
# large_json = JSON.parse(File.read('large_data.json'))
# StreamJSONParser.process_json_array_chunks(large_json['users'], 1000) do |chunk|
# puts "Processing chunk, size: #{chunk.length}"
# # Process each chunk of data
# endJSON Caching
ruby
require 'json'
require 'digest'
class JSONCache
def initialize(cache_dir = './json_cache')
@cache_dir = cache_dir
Dir.mkdir(@cache_dir) unless Dir.exist?(@cache_dir)
end
def fetch(key, expires_in = 3600)
cache_file = cache_file_path(key)
if File.exist?(cache_file)
cache_data = JSON.parse(File.read(cache_file))
if Time.now.to_i < cache_data['expires_at']
return cache_data['data']
else
File.delete(cache_file)
end
end
# Cache miss, execute block and cache result
data = yield
cache_data = {
data: data,
expires_at: Time.now.to_i + expires_in
}
File.write(cache_file, JSON.generate(cache_data))
data
end
private
def cache_file_path(key)
hash = Digest::MD5.hexdigest(key)
File.join(@cache_dir, "#{hash}.json")
end
end
# Using JSON cache
# cache = JSONCache.new
#
# # Cache expensive computation results
# result = cache.fetch('expensive_operation') do
# puts "Executing expensive operation..."
# sleep(2) # Simulate time-consuming operation
# { message: 'Operation completed', timestamp: Time.now.to_i }
# end
#
# puts JSON.pretty_generate(result)🔍 JSON Debugging Tools
JSON Formatting and Validation
ruby
require 'json'
class JSONDebugger
# Format JSON string
def self.format_json(json_string, options = {})
begin
data = JSON.parse(json_string)
JSON.pretty_generate(data, options)
rescue JSON::ParserError => e
"Invalid JSON: #{e.message}"
end
end
# Validate JSON string
def self.validate_json(json_string)
begin
JSON.parse(json_string)
{ valid: true, error: nil }
rescue JSON::ParserError => e
{ valid: false, error: e.message }
end
end
# Compare two JSON objects
def self.compare_json(json1, json2)
data1 = json1.is_a?(String) ? JSON.parse(json1) : json1
data2 = json2.is_a?(String) ? JSON.parse(json2) : json2
if data1 == data2
{ equal: true, differences: [] }
else
differences = find_differences(data1, data2)
{ equal: false, differences: differences }
end
end
private
def self.find_differences(obj1, obj2, path = '')
differences = []
case [obj1, obj2].map(&:class)
when [Hash, Hash]
all_keys = (obj1.keys + obj2.keys).uniq
all_keys.each do |key|
current_path = path.empty? ? key.to_s : "#{path}.#{key}"
if !obj1.key?(key)
differences << { path: current_path, type: 'missing_in_first', value: obj2[key] }
elsif !obj2.key?(key)
differences << { path: current_path, type: 'missing_in_second', value: obj1[key] }
else
differences.concat(find_differences(obj1[key], obj2[key], current_path))
end
end
when [Array, Array]
max_length = [obj1.length, obj2.length].max
(0...max_length).each do |i|
current_path = "#{path}[#{i}]"
if i >= obj1.length
differences << { path: current_path, type: 'missing_in_first', value: obj2[i] }
elsif i >= obj2.length
differences << { path: current_path, type: 'missing_in_second', value: obj1[i] }
else
differences.concat(find_differences(obj1[i], obj2[i], current_path))
end
end
else
if obj1 != obj2
differences << { path: path, type: 'different_values', first: obj1, second: obj2 }
end
end
differences
end
end
# Using debugging tools
# Format JSON
messy_json = '{"name":"John","age":25,"hobbies":["Reading","Swimming"],"address":{"city":"Beijing","district":"Chaoyang"}}'
formatted = JSONDebugger.format_json(messy_json)
puts "Formatted JSON:"
puts formatted
# Validate JSON
valid_json = '{"name": "John", "age": 25}'
invalid_json = '{"name": "John", "age": 25' # Missing closing brace
puts "\nValidation results:"
puts "Valid JSON: #{JSONDebugger.validate_json(valid_json)[:valid]}"
puts "Invalid JSON: #{JSONDebugger.validate_json(invalid_json)[:valid]}"
# Compare JSON
json1 = '{"name": "John", "age": 25, "city": "Beijing"}'
json2 = '{"name": "John", "age": 26, "city": "Shanghai"}'
comparison = JSONDebugger.compare_json(json1, json2)
puts "\nComparison results:"
puts "Equal: #{comparison[:equal]}"
puts "Differences:"
comparison[:differences].each do |diff|
puts " Path: #{diff[:path]}, Type: #{diff[:type]}"
end🎯 JSON Best Practices
1. Error Handling
ruby
require 'json'
class SafeJSONHandler
# Safely parse JSON
def self.parse(json_string, options = {})
JSON.parse(json_string, options)
rescue JSON::ParserError => e
raise "JSON parsing failed: #{e.message} (string: #{json_string[0, 100]}...)"
end
# Safely generate JSON
def self.generate(data, options = {})
JSON.generate(data, options)
rescue JSON::GeneratorError => e
raise "JSON generation failed: #{e.message}"
end
# Parsing with defaults
def self.parse_with_defaults(json_string, defaults = {})
parsed = parse(json_string, symbolize_names: true)
defaults.merge(parsed)
rescue
defaults
end
end
# Using safe handling
begin
valid_json = '{"name": "John", "age": 25}'
data = SafeJSONHandler.parse(valid_json, symbolize_names: true)
puts "Parsing successful: #{data}"
invalid_json = '{"name": "John", "age": 25'
data = SafeJSONHandler.parse(invalid_json)
rescue => e
puts "Caught error: #{e.message}"
end
# Using defaults
defaults = { name: 'Unknown', age: 0, city: 'Unknown' }
json_with_missing_fields = '{"name": "Jane", "age": 30}'
result = SafeJSONHandler.parse_with_defaults(json_with_missing_fields, defaults)
puts "Merged result: #{result}"2. Data Type Handling
ruby
require 'json'
# Handling special data types
class JSONTypeHandler
# Custom serialization options
def self.serialize_with_types(data)
JSON.generate(data, {
ascii_only: false,
max_nesting: 100
})
end
# Handling large numbers
def self.handle_large_numbers
# JavaScript number precision limit
large_number = 9007199254740991 # Number.MAX_SAFE_INTEGER
data = { big_number: large_number }
json_string = JSON.generate(data)
parsed = JSON.parse(json_string)
puts "Original number: #{large_number}"
puts "JSON string: #{json_string}"
puts "Parsed number: #{parsed['big_number']}"
puts "Equal: #{large_number == parsed['big_number']}"
end
# Handling datetime
def self.handle_datetime
now = Time.now
data = {
created_at: now,
updated_at: now.iso8601
}
# Default serialization
json_default = JSON.generate(data)
puts "Default serialization: #{json_default}"
# Custom serialization
json_custom = JSON.generate(data) do |obj|
case obj
when Time, Date, DateTime
obj.iso8601
else
obj
end
end
puts "Custom serialization: #{json_custom}"
end
end
# JSONTypeHandler.handle_large_numbers
# JSONTypeHandler.handle_datetime3. Secure Handling
ruby
require 'json'
class SecureJSONHandler
# Prevent deep nesting attacks
def self.safe_parse(json_string, max_depth = 10)
JSON.parse(json_string, max_nesting: max_depth)
rescue JSON::NestingError
raise "JSON nesting depth exceeds limit (max: #{max_depth})"
rescue JSON::ParserError => e
raise "JSON parsing error: #{e.message}"
end
# Limit JSON size
def self.parse_with_size_limit(json_string, max_size = 1024 * 1024) # 1MB
if json_string.bytesize > max_size
raise "JSON size exceeds limit (max: #{max_size} bytes)"
end
JSON.parse(json_string)
end
# Sanitize potentially dangerous content
def self.sanitize_json(data)
case data
when Hash
data.each_with_object({}) do |(key, value), result|
# Remove potentially dangerous keys
clean_key = key.to_s.gsub(/[<>]/, '')
result[clean_key] = sanitize_json(value)
end
when Array
data.map { |item| sanitize_json(item) }
when String
# Remove potential script tags
data.gsub(/<script.*?>.*?<\/script>/mi, '')
else
data
end
end
end
# Using secure handling
begin
# Normal JSON
normal_json = '{"name": "John", "age": 25}'
data = SecureJSONHandler.safe_parse(normal_json)
puts "Normal parsing: #{data}"
# Deep nesting JSON (simulating attack)
# nested_json = '{"a":{"b":{"c":{"d":{"e":{"f":{"g":{"h":{"i":{"j":{"k":"too deep"}}}}}}}}}}}'
# data = SecureJSONHandler.safe_parse(nested_json)
rescue => e
puts "Security error: #{e.message}"
end📚 Next Steps
After mastering Ruby JSON handling, we recommend continuing to learn:
- Ruby Reference Manual and Learning Resources - Get more learning materials
- Ruby Database Access - Learn database operations
- Ruby Web Services - Deep dive into Web development
- Ruby Multithreading - Learn about concurrent programming
Continue your Ruby learning journey!