Overview
Ruby provides JSON parsing and generation through the JSON
module in its standard library. The module converts between Ruby data structures (Hash, Array, String, Numeric, Boolean, nil) and JSON format strings. Ruby's JSON implementation handles the bidirectional transformation while preserving data types where possible.
The primary interface consists of JSON.parse
for converting JSON strings to Ruby objects and JSON.generate
or to_json
for converting Ruby objects to JSON strings. The module also provides streaming parsers for large JSON documents and customizable serialization options.
require 'json'
# Parse JSON string to Ruby hash
json_string = '{"name": "Alice", "age": 30, "active": true}'
data = JSON.parse(json_string)
# => {"name"=>"Alice", "age"=>30, "active"=>true}
# Generate JSON from Ruby hash
hash = { name: "Bob", age: 25, active: false }
JSON.generate(hash)
# => '{"name":"Bob","age":25,"active":false}'
# Alternative generation syntax
hash.to_json
# => '{"name":"Bob","age":25,"active":false}'
The JSON module automatically handles Ruby's core data types. Hash objects become JSON objects, Array objects become JSON arrays, and Ruby's true
, false
, and nil
map directly to their JSON equivalents. Numeric types (Integer, Float) convert to JSON numbers, while String objects become JSON strings with proper escaping.
Ruby stores parsed JSON object keys as strings by default, not symbols. This behavior differs from hash literals but maintains compatibility with JSON's string-based key specification. The module provides options to modify this behavior when parsing.
Basic Usage
The JSON.parse
method converts JSON strings into corresponding Ruby data structures. The parser handles nested objects and arrays, maintaining the hierarchical structure of the original JSON.
require 'json'
# Parse simple JSON object
user_json = '{"id": 123, "username": "john_doe", "verified": true}'
user = JSON.parse(user_json)
# => {"id"=>123, "username"=>"john_doe", "verified"=>true}
# Parse JSON array
numbers_json = '[1, 2, 3, 4, 5]'
numbers = JSON.parse(numbers_json)
# => [1, 2, 3, 4, 5]
# Parse nested JSON structure
nested_json = '{
"users": [
{"name": "Alice", "roles": ["admin", "editor"]},
{"name": "Bob", "roles": ["viewer"]}
],
"total": 2
}'
data = JSON.parse(nested_json)
# => {"users"=>[{"name"=>"Alice", "roles"=>["admin", "editor"]},
# {"name"=>"Bob", "roles"=>["viewer"]}], "total"=>2}
The JSON.generate
method converts Ruby objects into JSON strings. The method accepts Hash, Array, String, Numeric, Boolean, and nil values, serializing them according to JSON specification.
# Generate JSON from various Ruby objects
person = {
name: "Sarah",
age: 28,
skills: ["Ruby", "Python", "JavaScript"],
active: true,
manager: nil
}
json_output = JSON.generate(person)
# => '{"name":"Sarah","age":28,"skills":["Ruby","Python","JavaScript"],"active":true,"manager":null}'
# Generate JSON from array
languages = ["Ruby", "Python", "Go", "Rust"]
JSON.generate(languages)
# => '["Ruby","Python","Go","Rust"]'
# The to_json method provides identical functionality
person.to_json
# => '{"name":"Sarah","age":28,"skills":["Ruby","Python","JavaScript"],"active":true,"manager":null}'
Ruby's JSON module provides options to control parsing and generation behavior. The symbolize_names
option converts JSON object keys from strings to symbols during parsing.
json_data = '{"first_name": "John", "last_name": "Smith"}'
# Default parsing (string keys)
parsed_strings = JSON.parse(json_data)
# => {"first_name"=>"John", "last_name"=>"Smith"}
# Parse with symbol keys
parsed_symbols = JSON.parse(json_data, symbolize_names: true)
# => {:first_name=>"John", :last_name=>"Smith"}
The module handles Unicode characters and escape sequences automatically. JSON strings containing escaped characters convert to their unescaped Ruby string equivalents, while Ruby strings with Unicode characters serialize properly to JSON.
# Parse JSON with escaped characters
escaped_json = '{"message": "Hello\\nWorld", "emoji": "\\u2764\\ufe0f"}'
parsed = JSON.parse(escaped_json)
# => {"message"=>"Hello\nWorld", "emoji"=>"❤️"}
# Generate JSON with Unicode
unicode_data = { greeting: "Héllo Wørld! 🌍", count: 42 }
JSON.generate(unicode_data)
# => '{"greeting":"Héllo Wørld! 🌍","count":42}'
Error Handling & Debugging
JSON parsing operations raise JSON::ParserError
exceptions when encountering invalid JSON syntax. The error messages indicate the specific parsing failure location and nature.
require 'json'
# Handle malformed JSON
begin
invalid_json = '{"name": "Alice", "age":}' # Missing value
JSON.parse(invalid_json)
rescue JSON::ParserError => e
puts "JSON parsing failed: #{e.message}"
# => JSON parsing failed: unexpected token at '}'
end
# Handle unexpected end of input
begin
incomplete_json = '{"users": [{"name": "Bob"' # Incomplete structure
JSON.parse(incomplete_json)
rescue JSON::ParserError => e
puts "Incomplete JSON: #{e.message}"
# => Incomplete JSON: unexpected end of JSON input
end
The JSON module provides validation methods to check JSON strings before parsing. The JSON.valid_json?
method returns a boolean indicating whether a string contains valid JSON without raising exceptions.
# Validate JSON before parsing
def safe_parse(json_string)
return nil unless JSON.valid_json?(json_string)
JSON.parse(json_string)
rescue JSON::ParserError
nil
end
valid_json = '{"status": "success"}'
invalid_json = '{"status": success}' # Unquoted string value
safe_parse(valid_json) # => {"status"=>"success"}
safe_parse(invalid_json) # => nil
Ruby objects that cannot serialize to JSON raise JSON::GeneratorError
exceptions during generation. Objects without defined JSON serialization methods cause these errors.
class CustomObject
def initialize(data)
@data = data
end
end
# Attempt to serialize unsupported object
begin
custom = CustomObject.new("test")
JSON.generate({object: custom})
rescue JSON::GeneratorError => e
puts "Cannot serialize: #{e.message}"
end
Complex JSON structures may contain deeply nested objects that exceed parsing limits. The JSON module provides options to control recursion depth and prevent stack overflow errors.
# Handle deeply nested structures
deep_json = '{"level1": {"level2": {"level3": {"level4": "value"}}}}'
begin
# Parse with depth limit
result = JSON.parse(deep_json, max_nesting: 3)
rescue JSON::NestingError => e
puts "Nesting too deep: #{e.message}"
end
Debugging JSON parsing issues often requires examining the exact character position where parsing fails. Ruby's JSON error messages include position information for syntax errors.
# Debug JSON parsing with detailed error information
def debug_json_parse(json_string)
JSON.parse(json_string)
rescue JSON::ParserError => e
error_position = e.message.match(/at position (\d+)/)
if error_position
position = error_position[1].to_i
context_start = [0, position - 10].max
context_end = [json_string.length, position + 10].min
context = json_string[context_start...context_end]
puts "Parse error near: '#{context}'"
puts "Error position: #{position}"
end
puts "Full error: #{e.message}"
nil
end
Encoding issues can cause JSON parsing failures when source strings contain invalid UTF-8 sequences. Ruby's string encoding methods help identify and resolve these issues.
# Handle encoding issues
def parse_with_encoding_check(json_string)
# Ensure valid UTF-8 encoding
unless json_string.valid_encoding?
json_string = json_string.encode('UTF-8', 'UTF-8', invalid: :replace)
end
JSON.parse(json_string)
rescue JSON::ParserError => e
puts "Encoding or syntax error: #{e.message}"
nil
end
Performance & Memory
JSON parsing performance depends on document size and structure complexity. Large JSON documents with deep nesting consume more memory and processing time than flat structures.
require 'json'
require 'benchmark'
# Performance comparison between parsing approaches
large_array = (1..10000).map { |i| { id: i, name: "User#{i}", active: i.odd? } }
json_string = JSON.generate(large_array)
Benchmark.bm(15) do |x|
x.report("Standard parse:") do
1000.times { JSON.parse(json_string) }
end
x.report("Symbol keys:") do
1000.times { JSON.parse(json_string, symbolize_names: true) }
end
end
Memory usage increases significantly when parsing large JSON documents because Ruby creates all objects in memory simultaneously. For memory-constrained environments, consider streaming approaches or processing JSON in smaller chunks.
# Memory-efficient JSON processing for large datasets
def process_large_json_file(filename)
File.open(filename, 'r') do |file|
# Read and process line by line for JSON Lines format
file.each_line do |line|
begin
record = JSON.parse(line.strip)
yield record if block_given?
rescue JSON::ParserError
# Skip invalid lines
next
end
end
end
end
# Process large dataset without loading everything into memory
process_large_json_file('large_dataset.jsonl') do |record|
puts record['id'] if record['status'] == 'active'
end
JSON generation performance varies based on object complexity and serialization options. Simple objects with primitive values serialize faster than complex nested structures.
# Compare generation performance for different data structures
simple_data = { id: 1, name: "test", active: true }
complex_data = {
users: (1..1000).map do |i|
{
id: i,
profile: {
name: "User #{i}",
settings: { theme: "dark", notifications: true },
metadata: { created: Time.now, updated: Time.now }
}
}
end
}
Benchmark.bm(15) do |x|
x.report("Simple object:") do
10000.times { JSON.generate(simple_data) }
end
x.report("Complex object:") do
100.times { JSON.generate(complex_data) }
end
end
String allocation becomes a bottleneck when generating large JSON documents. Ruby's JSON module creates intermediate string objects during serialization, which impacts garbage collection.
# Monitor memory usage during JSON operations
def measure_memory_usage(&block)
GC.start
memory_before = `ps -o rss= -p #{Process.pid}`.to_i
result = yield
GC.start
memory_after = `ps -o rss= -p #{Process.pid}`.to_i
memory_diff = memory_after - memory_before
puts "Memory usage: #{memory_diff} KB"
result
end
# Test memory usage for JSON generation
large_data = { items: Array.new(50000) { { id: rand(10000), value: rand } } }
measure_memory_usage do
JSON.generate(large_data)
end
Ruby provides streaming JSON generation through the JSON::Stream::Writer
class for applications that need to generate large JSON documents without holding the entire structure in memory.
require 'json/stream'
# Stream large JSON arrays to reduce memory footprint
def generate_large_json_stream(output_file)
File.open(output_file, 'w') do |file|
writer = JSON::Stream::Writer.new(file)
writer.start_array
10000.times do |i|
writer.value({
id: i,
timestamp: Time.now.to_f,
data: "Record #{i}"
})
end
writer.end_array
end
end
Production Patterns
Web applications commonly receive JSON data from API requests and database storage. Ruby frameworks provide integration patterns for handling JSON in HTTP request/response cycles.
# Rails controller pattern for JSON APIs
class UsersController < ApplicationController
def create
begin
user_params = JSON.parse(request.body.read)
user = User.create!(user_params)
render json: { status: 'success', user: user }, status: 201
rescue JSON::ParserError
render json: { error: 'Invalid JSON format' }, status: 400
rescue ActiveRecord::RecordInvalid => e
render json: { error: e.message }, status: 422
end
end
def index
users = User.active.limit(100)
render json: {
users: users.as_json(only: [:id, :name, :email]),
total: users.count
}
end
end
Configuration management often uses JSON files for storing application settings. Production systems require robust JSON configuration loading with validation and fallback mechanisms.
class ConfigurationManager
def self.load_config(config_path, environment = 'production')
config_data = File.read(config_path)
all_configs = JSON.parse(config_data, symbolize_names: true)
environment_config = all_configs[environment.to_sym]
raise "Configuration for #{environment} not found" unless environment_config
validate_required_keys(environment_config)
environment_config
rescue JSON::ParserError => e
raise "Invalid JSON in configuration file: #{e.message}"
rescue Errno::ENOENT
raise "Configuration file not found: #{config_path}"
end
private
def self.validate_required_keys(config)
required_keys = [:database_url, :secret_key, :api_endpoint]
missing_keys = required_keys - config.keys
unless missing_keys.empty?
raise "Missing required configuration keys: #{missing_keys.join(', ')}"
end
end
end
# Usage in application initialization
begin
config = ConfigurationManager.load_config('config/app.json', ENV['RAILS_ENV'])
puts "Loaded configuration for #{ENV['RAILS_ENV']} environment"
rescue => e
puts "Configuration error: #{e.message}"
exit 1
end
API clients frequently handle JSON responses from external services. Production applications implement retry logic, timeout handling, and response validation for reliable API integration.
require 'net/http'
require 'uri'
require 'json'
class ApiClient
def initialize(base_url, timeout: 30)
@base_url = base_url
@timeout = timeout
end
def get_user(user_id)
uri = URI("#{@base_url}/users/#{user_id}")
response = make_request(uri)
JSON.parse(response.body, symbolize_names: true)
rescue JSON::ParserError => e
raise ApiError, "Invalid JSON response: #{e.message}"
end
def create_user(user_data)
uri = URI("#{@base_url}/users")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
http.read_timeout = @timeout
request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request.body = JSON.generate(user_data)
response = http.request(request)
handle_response(response)
end
private
def make_request(uri, retries = 3)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
http.read_timeout = @timeout
request = Net::HTTP::Get.new(uri)
response = http.request(request)
unless response.is_a?(Net::HTTPSuccess)
raise ApiError, "HTTP #{response.code}: #{response.message}"
end
response
rescue Net::TimeoutError => e
retries -= 1
retry if retries > 0
raise ApiError, "Request timeout after #{@timeout} seconds"
end
def handle_response(response)
case response
when Net::HTTPSuccess
JSON.parse(response.body, symbolize_names: true)
when Net::HTTPClientError
error_data = JSON.parse(response.body) rescue { message: response.body }
raise ApiError, "Client error: #{error_data['message']}"
when Net::HTTPServerError
raise ApiError, "Server error: #{response.code} #{response.message}"
else
raise ApiError, "Unexpected response: #{response.code} #{response.message}"
end
rescue JSON::ParserError
raise ApiError, "Invalid JSON in response body"
end
end
class ApiError < StandardError; end
Database interactions often involve storing and retrieving JSON data. Modern databases provide JSON column types that require proper serialization and deserialization handling.
# ActiveRecord model with JSON column handling
class UserPreferences < ActiveRecord::Base
# Assuming 'preferences' column is JSON type in database
def get_preference(key)
preferences&.dig(key.to_s)
end
def set_preference(key, value)
self.preferences ||= {}
self.preferences = preferences.merge(key.to_s => value)
end
def merge_preferences(new_prefs)
self.preferences = (preferences || {}).merge(new_prefs.stringify_keys)
end
# Custom serialization for API responses
def as_json(options = {})
super(options).tap do |json|
json['preferences'] = preferences || {}
end
end
end
# Usage patterns
user_prefs = UserPreferences.find(user_id)
user_prefs.set_preference(:theme, 'dark')
user_prefs.set_preference(:notifications, { email: true, sms: false })
user_prefs.save!
Common Pitfalls
JSON parsing converts object keys to strings, not symbols, which differs from Ruby hash literal syntax. This behavior causes key access failures when code expects symbol keys.
# Common mistake: assuming symbol keys after parsing
json_data = '{"user_id": 123, "username": "alice"}'
parsed = JSON.parse(json_data)
# This fails because keys are strings
user_id = parsed[:user_id] # => nil (key doesn't exist)
# Correct approaches
user_id = parsed['user_id'] # => 123
# OR
parsed = JSON.parse(json_data, symbolize_names: true)
user_id = parsed[:user_id] # => 123
Ruby's nil
values serialize to JSON null
, but parsing null
back returns Ruby nil
. However, JSON doesn't distinguish between missing keys and null
values, while Ruby hash access does.
# Difference between missing keys and null values
data = { name: "Alice", age: nil }
json_string = JSON.generate(data) # => '{"name":"Alice","age":null}'
parsed = JSON.parse(json_string) # => {"name"=>"Alice", "age"=>nil}
# These behave differently
parsed.key?('age') # => true (key exists with nil value)
parsed.key?('missing') # => false (key doesn't exist)
parsed['age'] # => nil
parsed['missing'] # => nil (same return value!)
# Better approach for checking existence
def get_value_or_default(hash, key, default = 'N/A')
if hash.key?(key)
hash[key] || default # Handle nil values
else
default
end
end
Floating point precision issues occur when round-trip serializing numeric values through JSON. JSON numbers have different precision characteristics than Ruby's Float class.
# Precision loss with floating point numbers
original_number = 1.7976931348623157e+308
json_string = JSON.generate({ value: original_number })
parsed = JSON.parse(json_string)
parsed['value'] == original_number # May be false due to precision
# Handle precision-sensitive numbers
require 'bigdecimal'
class PreciseJsonHandler
def self.generate_with_precision(data)
# Convert BigDecimal to string for JSON
processed = convert_decimals_for_json(data)
JSON.generate(processed)
end
def self.parse_with_precision(json_string, decimal_keys = [])
data = JSON.parse(json_string)
convert_strings_to_decimals(data, decimal_keys)
end
private
def self.convert_decimals_for_json(obj)
case obj
when BigDecimal
obj.to_s
when Hash
obj.transform_values { |v| convert_decimals_for_json(v) }
when Array
obj.map { |v| convert_decimals_for_json(v) }
else
obj
end
end
def self.convert_strings_to_decimals(obj, keys)
case obj
when Hash
obj.transform_values do |v|
if keys.include?(obj.keys.find { |k| obj[k] == v })
BigDecimal(v.to_s)
else
convert_strings_to_decimals(v, keys)
end
end
when Array
obj.map { |v| convert_strings_to_decimals(v, keys) }
else
obj
end
end
end
Time and Date objects require special handling because JSON doesn't define standard datetime formats. Ruby's Time objects serialize to strings but don't automatically parse back to Time objects.
# Time serialization issues
current_time = Time.now
data = { timestamp: current_time, event: "user_login" }
json_string = JSON.generate(data)
# => '{"timestamp":"2024-01-15 10:30:45 -0500","event":"user_login"}'
parsed = JSON.parse(json_string)
parsed['timestamp'] # => "2024-01-15 10:30:45 -0500" (String, not Time)
# Custom Time handling
class TimeAwareJson
def self.generate(obj)
processed = convert_times_to_iso8601(obj)
JSON.generate(processed)
end
def self.parse(json_string, time_keys = [])
data = JSON.parse(json_string)
convert_iso8601_to_times(data, time_keys)
end
private
def self.convert_times_to_iso8601(obj)
case obj
when Time
obj.iso8601
when Hash
obj.transform_values { |v| convert_times_to_iso8601(v) }
when Array
obj.map { |v| convert_times_to_iso8601(v) }
else
obj
end
end
def self.convert_iso8601_to_times(obj, time_keys)
case obj
when Hash
obj.transform_values do |value|
key = obj.keys.find { |k| obj[k] == value }
if time_keys.include?(key) && value.is_a?(String)
Time.parse(value)
else
convert_iso8601_to_times(value, time_keys)
end
end
when Array
obj.map { |v| convert_iso8601_to_times(v, time_keys) }
else
obj
end
end
end
# Usage
json_string = TimeAwareJson.generate({ created_at: Time.now, name: "test" })
parsed = TimeAwareJson.parse(json_string, ['created_at'])
parsed['created_at'].class # => Time
Unicode and encoding issues arise when JSON strings contain invalid UTF-8 sequences or when source data has encoding problems. These issues cause parsing failures or data corruption.
# Handle encoding issues gracefully
def safe_json_parse(json_string)
# Ensure string is properly encoded
clean_string = json_string.encode('UTF-8', 'UTF-8',
invalid: :replace,
undef: :replace,
replace: '?'
)
JSON.parse(clean_string)
rescue Encoding::UndefinedConversionError => e
puts "Encoding conversion failed: #{e.message}"
nil
rescue JSON::ParserError => e
puts "JSON parsing failed: #{e.message}"
nil
end
# Test with problematic encoding
problematic_json = '{"message": "Hello\xC0World"}'.force_encoding('UTF-8')
result = safe_json_parse(problematic_json)
Circular reference errors occur when Ruby objects contain references to themselves or create reference cycles. JSON generation fails when encountering these structures.
# Circular reference handling
class CircularReferenceHandler
def initialize
@seen_objects = Set.new
end
def safe_generate(obj, max_depth = 10, current_depth = 0)
return '"[Max Depth Reached]"' if current_depth > max_depth
case obj
when Hash
return '"[Circular Reference]"' if @seen_objects.include?(obj.object_id)
@seen_objects.add(obj.object_id)
hash_json = obj.map do |k, v|
key_json = safe_generate(k, max_depth, current_depth + 1)
value_json = safe_generate(v, max_depth, current_depth + 1)
"#{key_json}:#{value_json}"
end.join(',')
@seen_objects.delete(obj.object_id)
"{#{hash_json}}"
when Array
return '"[Circular Reference]"' if @seen_objects.include?(obj.object_id)
@seen_objects.add(obj.object_id)
array_json = obj.map do |item|
safe_generate(item, max_depth, current_depth + 1)
end.join(',')
@seen_objects.delete(obj.object_id)
"[#{array_json}]"
else
JSON.generate(obj)
end
rescue JSON::GeneratorError
'"[Unserializable Object]"'
end
end
Reference
Core Methods
Method | Parameters | Returns | Description |
---|---|---|---|
JSON.parse(string, **opts) |
string (String), options (Hash) |
Object |
Parse JSON string to Ruby object |
JSON.generate(obj, **opts) |
obj (Object), options (Hash) |
String |
Generate JSON string from Ruby object |
JSON.valid_json?(string) |
string (String) |
Boolean |
Check if string is valid JSON |
#to_json(**opts) |
options (Hash) | String |
Convert object to JSON string |
Parse Options
Option | Type | Default | Description |
---|---|---|---|
symbolize_names |
Boolean | false | Convert object keys to symbols |
max_nesting |
Integer | 100 | Maximum nesting depth |
allow_nan |
Boolean | false | Allow NaN and Infinity values |
create_additions |
Boolean | false | Create objects from JSON additions |
Generate Options
Option | Type | Default | Description |
---|---|---|---|
max_nesting |
Integer | 100 | Maximum nesting depth |
allow_nan |
Boolean | false | Allow NaN and Infinity values |
indent |
String | nil | Indentation string for pretty printing |
space |
String | nil | Space after colons and commas |
space_before |
String | nil | Space before colons |
object_nl |
String | nil | Newline after objects |
array_nl |
String | nil | Newline after arrays |
Exception Classes
Exception | Description | Common Causes |
---|---|---|
JSON::ParserError |
JSON parsing failed | Invalid JSON syntax, unexpected tokens |
JSON::GeneratorError |
JSON generation failed | Unsupported object types, circular references |
JSON::NestingError |
Exceeded nesting limit | Too deeply nested structures |
Data Type Mapping
Ruby Type | JSON Type | Notes |
---|---|---|
Hash |
Object | Keys converted to strings by default |
Array |
Array | Order preserved |
String |
String | UTF-8 encoded |
Integer/Float |
Number | Precision may be lost with very large numbers |
true/false |
Boolean | Direct mapping |
nil |
null | Direct mapping |
Symbol |
String | Converted to string representation |
Common Patterns
# Parse with error handling
begin
data = JSON.parse(json_string, symbolize_names: true)
rescue JSON::ParserError => e
# Handle parse error
end
# Generate with pretty printing
JSON.generate(data, indent: ' ', space: ' ', object_nl: "\n", array_nl: "\n")
# Custom serialization
class User
def as_json(options = {})
{ id: @id, name: @name, email: @email }
end
def to_json(options = {})
as_json(options).to_json
end
end
# Safe parsing with validation
def parse_json_safely(string)
return nil unless string.is_a?(String) && !string.empty?
JSON.parse(string)
rescue JSON::ParserError
nil
end
Performance Considerations
Operation | Memory Usage | Speed | Notes |
---|---|---|---|
JSON.parse |
High for large docs | Fast | Loads entire structure into memory |
JSON.generate |
Moderate | Fast | String building overhead |
symbolize_names: true |
Higher | Slower | Additional symbol creation |
Deep nesting | High | Slower | Recursive processing overhead |