Skip to content

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_output

JSON 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
# end

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

3. 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:

Continue your Ruby learning journey!

Content is for learning and research only.