Working with JSON in Ruby

Ruby has built-in support for JSON through the json library. This guide covers parsing, generation, validation, and best practices for handling JSON in Ruby.

Parsing JSON

Use JSON.parse() to convert JSON strings to Ruby objects:

require 'json'

# Parse JSON string to Hash
json_string = '{"name": "John", "age": 30, "city": "New York"}'
data = JSON.parse(json_string)

puts data["name"]  # John
puts data["age"]   # 30
puts data.class    # Hash

# Parse JSON array
json_array = '[1, 2, 3, "four", true, null]'
items = JSON.parse(json_array)
puts items.inspect  # [1, 2, 3, "four", true, nil]

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

Generating JSON

Use JSON.generate() or .to_json to convert Ruby objects to JSON:

require 'json'

user = {
  name: "Jane",
  age: 25,
  is_active: true,
  hobbies: ["reading", "coding"],
  address: nil
}

# Basic generation
json = JSON.generate(user)
puts json
# {"name":"Jane","age":25,"is_active":true,"hobbies":["reading","coding"],"address":null}

# Using to_json method
json = user.to_json
puts json

# Pretty print
pretty_json = JSON.pretty_generate(user)
puts pretty_json
# {
#   "name": "Jane",
#   "age": 25,
#   "is_active": true,
#   "hobbies": [
#     "reading",
#     "coding"
#   ],
#   "address": null
# }

# Custom indentation
json = JSON.pretty_generate(user, indent: '    ', space: ' ')
puts json

Working with Files

require 'json'

# Read JSON file
json_content = File.read('data.json')
data = JSON.parse(json_content)

# Or in one line
data = JSON.parse(File.read('data.json'))

# Write JSON file
user = { name: "John", age: 30 }
File.write('output.json', JSON.pretty_generate(user))

# Read and parse with error handling
def read_json_file(path)
  JSON.parse(File.read(path), symbolize_names: true)
rescue Errno::ENOENT
  puts "File not found: #{path}"
  nil
rescue JSON::ParserError => e
  puts "Invalid JSON: #{e.message}"
  nil
end

# Append to existing JSON file
def append_to_json_array(path, new_item)
  data = JSON.parse(File.read(path)) rescue []
  data << new_item
  File.write(path, JSON.pretty_generate(data))
end

JSON Validation

require 'json'

def valid_json?(string)
  JSON.parse(string)
  true
rescue JSON::ParserError
  false
end

puts valid_json?('{"name": "John"}')  # true
puts valid_json?('{invalid}')         # false

# Detailed validation
def validate_json(string)
  data = JSON.parse(string)
  {
    valid: true,
    data: data,
    type: data.class.name
  }
rescue JSON::ParserError => e
  {
    valid: false,
    error: e.message
  }
end

result = validate_json('{"name": "John"}')
puts result
# {:valid=>true, :data=>{"name"=>"John"}, :type=>"Hash"}

result = validate_json('{invalid}')
puts result
# {:valid=>false, :error=>"unexpected token at '{invalid}'"}

Custom JSON Serialization

require 'json'
require 'date'

class User
  attr_accessor :name, :age, :email, :created_at

  def initialize(name, age, email)
    @name = name
    @age = age
    @email = email
    @created_at = DateTime.now
  end

  # Define how to convert to JSON
  def to_json(*args)
    {
      name: @name,
      age: @age,
      email: @email,
      created_at: @created_at.iso8601
    }.to_json(*args)
  end

  # Class method to create from JSON
  def self.from_json(json_string)
    data = JSON.parse(json_string, symbolize_names: true)
    user = new(data[:name], data[:age], data[:email])
    user.created_at = DateTime.parse(data[:created_at]) if data[:created_at]
    user
  end
end

# Usage
user = User.new("John", 30, "john@example.com")
json = user.to_json
puts json
# {"name":"John","age":30,"email":"john@example.com","created_at":"2024-01-15T10:30:00+00:00"}

# Recreate from JSON
user2 = User.from_json(json)
puts user2.name  # John

Working with APIs

require 'json'
require 'net/http'
require 'uri'

# GET request
def fetch_json(url)
  uri = URI.parse(url)
  response = Net::HTTP.get_response(uri)

  if response.is_a?(Net::HTTPSuccess)
    JSON.parse(response.body)
  else
    raise "HTTP Error: #{response.code}"
  end
end

data = fetch_json('https://api.example.com/users')
puts data

# POST request with JSON body
def post_json(url, data)
  uri = URI.parse(url)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = uri.scheme == 'https'

  request = Net::HTTP::Post.new(uri.path)
  request['Content-Type'] = 'application/json'
  request.body = data.to_json

  response = http.request(request)
  JSON.parse(response.body)
end

result = post_json('https://api.example.com/users', {
  name: 'John',
  email: 'john@example.com'
})

# Using Faraday gem (recommended for production)
# gem install faraday
require 'faraday'

conn = Faraday.new(url: 'https://api.example.com') do |f|
  f.request :json
  f.response :json
end

response = conn.get('/users')
puts response.body  # Already parsed as Ruby Hash

response = conn.post('/users', { name: 'John' })
puts response.body

JSON with ActiveRecord (Rails)

# In Rails, ActiveRecord models automatically support JSON serialization

class User < ApplicationRecord
  # Serialize a column as JSON
  serialize :preferences, JSON

  # Or in Rails 5+
  serialize :preferences, coder: JSON

  # Store accessor for JSON column (PostgreSQL/MySQL JSON columns)
  store :settings, accessors: [:theme, :notifications], coder: JSON
end

# Usage
user = User.new
user.preferences = { theme: 'dark', language: 'en' }
user.save

# Access nested JSON
user.preferences['theme']  # 'dark'

# Store accessors
user.theme = 'light'
user.notifications = true

# Render as JSON in controller
class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
    render json: @user
  end

  def index
    @users = User.all
    render json: @users, only: [:id, :name, :email]
  end
end

JSON Schema Validation

# gem install json-schema
require 'json-schema'

schema = {
  "type" => "object",
  "required" => ["name", "email"],
  "properties" => {
    "name" => { "type" => "string", "minLength" => 1 },
    "age" => { "type" => "integer", "minimum" => 0 },
    "email" => { "type" => "string", "format" => "email" }
  }
}

# Validate data
data = { "name" => "John", "email" => "john@example.com", "age" => 30 }

if JSON::Validator.validate(schema, data)
  puts "Valid!"
else
  puts "Invalid!"
end

# Get validation errors
errors = JSON::Validator.fully_validate(schema, data)
puts errors.inspect

# Validate with exception
begin
  JSON::Validator.validate!(schema, data)
rescue JSON::Schema::ValidationError => e
  puts "Validation error: #{e.message}"
end

Performance with Oj

# gem install oj
require 'oj'

# Oj is 2-3x faster than the standard JSON library

# Parse JSON
data = Oj.load('{"name": "John", "age": 30}')

# Generate JSON
json = Oj.dump({ name: "Jane", age: 25 })

# Pretty print
json = Oj.dump({ name: "Jane" }, indent: 2)

# Different modes
Oj.default_options = { mode: :compat }  # Compatible with JSON gem
Oj.default_options = { mode: :object }  # Preserve Ruby object types

# Use as drop-in replacement
require 'oj'
Oj.mimic_JSON()

# Now JSON.parse and JSON.generate use Oj
data = JSON.parse('{"name": "John"}')
json = JSON.generate({ name: "Jane" })

Common Patterns

require 'json'

# Deep symbolize keys
def deep_symbolize_keys(hash)
  hash.transform_keys(&:to_sym).transform_values do |value|
    case value
    when Hash then deep_symbolize_keys(value)
    when Array then value.map { |v| v.is_a?(Hash) ? deep_symbolize_keys(v) : v }
    else value
    end
  end
end

# Merge JSON objects
def merge_json(json1, json2)
  data1 = JSON.parse(json1)
  data2 = JSON.parse(json2)
  data1.merge(data2).to_json
end

# Extract nested value safely
def dig_json(json_string, *keys)
  JSON.parse(json_string).dig(*keys)
rescue JSON::ParserError
  nil
end

# Usage
json = '{"user": {"address": {"city": "NYC"}}}'
city = dig_json(json, "user", "address", "city")  # "NYC"

Best Practices