Working with JSON
JSON (JavaScript Object Notation) is the de facto format for exchanging structured data between programs, whether you’re calling a web API, reading a configuration file, or writing test fixtures to disk. Ruby ships with a JSON library in its standard library, so turning JSON text into native Ruby objects — and back again — takes nothing more than a require 'json' line. This lesson covers parsing, generating, pretty-printing, the string-vs-symbol key gotcha, and the mistakes that trip up almost everyone the first time they touch JSON in Ruby.
Overview: How JSON Works in Ruby
JSON is a lightweight, text-based data format built around exactly six kinds of values: objects ({...}), arrays ([...]), strings, numbers, booleans (true/false), and null. Ruby’s standard library ships a JSON module that maps each of those onto a native Ruby type, so once you parse a document you’re working with ordinary hashes, arrays, strings, and numbers — nothing exotic, and every method you already know on Hash and Array (each, map, select, dig) works immediately.
require 'json' loads the module and, importantly, also reopens Ruby’s core classes (Hash, Array, String, Integer, Float, NilClass, TrueClass, FalseClass, and Symbol) to add a to_json instance method to each. That’s why, after requiring the library, you can call .to_json directly on a plain hash or array without wrapping it in anything else. Because almost everything in Ruby is an object, this is really just ordinary method mixing-in — the JSON library extends existing classes rather than inventing new syntax.
The two directions you’ll use constantly:
- Parsing — turning a JSON string into Ruby objects with
JSON.parse. - Generating — turning Ruby objects into a JSON string with
JSON.generate,JSON.pretty_generate, orto_json.
| JSON type | Ruby type after JSON.parse |
|---|---|
object {} |
Hash |
array [] |
Array |
| string | String |
| number | Integer or Float |
true / false |
true / false |
null |
nil |
By default, JSON.parse returns a Hash with String keys, because JSON itself has no concept of a symbol — only strings exist as a key type. If your code expects to index a parsed hash with symbols (data[:name] instead of data["name"]), pass the symbolize_names: true option. The reverse also holds: when you generate JSON from a hash that uses symbol keys or symbol values, JSON.generate converts every symbol to its string representation, because the JSON spec has no symbol type to serialize into. A symbol like :programmer always comes back out the other side as the string "programmer".
Because JSON’s null becomes Ruby’s nil, and Ruby’s truthiness treats only nil and false as falsy, a parsed value of false is just as “falsy” in an if check as a missing key that returns nil from Hash#[] — but they mean different things: an explicit false versus an absent key entirely. Don’t conflate if data["active"] with checking whether the key exists; use data.key?("active") when that distinction matters.
Syntax
JSON.parse(json_string, symbolize_names: true) # => Hash or Array
JSON.generate(ruby_object) # => compact JSON String
JSON.pretty_generate(ruby_object) # => indented JSON String
ruby_object.to_json # => same as JSON.generate
| Method | Purpose |
|---|---|
JSON.parse(str, symbolize_names: true) |
Convert a JSON string into Ruby objects; keys become symbols when the option is set (default is string keys) |
JSON.generate(obj) |
Convert a Ruby object into a compact, single-line JSON string |
JSON.pretty_generate(obj) |
Same as generate, but indented and newline-formatted for humans |
obj.to_json |
Instance method equivalent to JSON.generate(obj), available on core types after require 'json' |
JSON::ParserError |
The exception JSON.parse raises when given malformed JSON |
Examples
Example 1: Parsing a JSON string
require 'json'
json_string = '{"name": "Ada", "age": 36, "languages": ["Ruby", "Assembly"]}'
data = JSON.parse(json_string)
puts data.class
puts data["name"]
puts data["age"]
puts data["languages"].join(", ")
Output:
Hash
Ada
36
Ruby, Assembly
The JSON object became a Hash, the JSON array became an Array, and the JSON string and number became a Ruby String and Integer respectively. Notice the keys are plain strings ("name"), which is the default — nothing here asked for symbols.
Example 2: Generating JSON, pretty-printing, and round-tripping
require 'json'
person = { name: "Grace Hopper", role: :programmer, born: 1906 }
json_output = JSON.generate(person)
puts json_output
pretty_output = JSON.pretty_generate(person)
puts pretty_output
parsed_back = JSON.parse(json_output, symbolize_names: true)
puts parsed_back[:name]
puts parsed_back[:role]
Output:
{"name":"Grace Hopper","role":"programmer","born":1906}
{
"name": "Grace Hopper",
"role": "programmer",
"born": 1906
}
Grace Hopper
programmer
The symbol :programmer is generated as the plain string "programmer", since JSON has no symbol type. When it’s parsed back with symbolize_names: true, the key role becomes the symbol :role, but its value remains the string "programmer" — symbolize_names only affects keys, never values.
Example 3: Writing and reading a JSON file
require 'json'
inventory = [
{ sku: "A100", item: "Widget", qty: 42 },
{ sku: "B200", item: "Gadget", qty: 7 }
]
File.write("inventory.json", JSON.pretty_generate(inventory))
file_contents = File.read("inventory.json")
loaded = JSON.parse(file_contents, symbolize_names: true)
total = loaded.sum { |entry| entry[:qty] }
puts "Loaded #{loaded.size} items, total quantity: #{total}"
loaded.each do |entry|
puts "#{entry[:sku]}: #{entry[:item]} (#{entry[:qty]})"
end
Output:
Loaded 2 items, total quantity: 49
A100: Widget (42)
B200: Gadget (7)
This is the pattern you’ll use for config files, fixtures, and caches: serialize an array of hashes with pretty_generate so it’s readable in version control, write it with File.write, then read and re-parse it later with File.read plus JSON.parse. Because symbolize_names: true was used, every hash in loaded can be indexed with symbols like entry[:qty].
How It Works Step by Step
JSON.parsereads the source text and recursively descends into it: when it encounters{, it starts building aHash; when it encounters[, it starts building anArray.- For each key/value pair inside an object, the parser first fully resolves the value — which may itself be a nested object or array, parsed recursively — before adding it to the hash being built.
- Scalars (strings, numbers,
true,false,null) convert directly to their matching Ruby literal as soon as they’re read, with no further processing needed. - If
symbolize_names: trueis passed, every object key encountered during this walk is interned into aSymbolinstead of left as aString; values are untouched. JSON.generate(andto_json) works in the opposite direction: it walks the Ruby object tree recursively, emitting the matching JSON syntax for eachHash,Array,String, number, boolean, andnilit finds, converting anySymbolkey or value to a JSON string along the way.- If the source text is not valid JSON — an unmatched brace, single quotes instead of double quotes, a trailing comma — parsing stops immediately and
JSON.parseraisesJSON::ParserErrorrather than returning a partial result.
Common Mistakes
Mistake 1: Assuming parsed keys are symbols
JSON.parse returns string keys by default. Indexing the result with a symbol silently returns nil instead of raising an error, which makes the bug easy to miss:
require 'json'
json_string = '{"name": "Ada", "age": 36}'
data = JSON.parse(json_string)
puts "Name: #{data[:name].inspect}"
Output:
Name: nil
data has the key "name" (a String), not :name (a Symbol), so data[:name] looks up a key that was never there and quietly returns nil — no exception, just a wrong value further down the line. Fix it by requesting symbol keys at parse time:
require 'json'
json_string = '{"name": "Ada", "age": 36}'
data = JSON.parse(json_string, symbolize_names: true)
puts "Name: #{data[:name].inspect}"
Output:
Name: "Ada"
Mistake 2: Not handling malformed JSON
JSON requires double-quoted strings; single quotes, trailing commas, or unquoted keys are all invalid. Calling JSON.parse on such text without a rescue crashes the program the moment untrusted input (a file, an API response, user input) doesn’t match the spec:
require 'json'
malformed = "{'name': 'Ada'}"
data = JSON.parse(malformed)
puts data["name"]
This raises JSON::ParserError before the puts line ever runs, because {'name': 'Ada'} uses single quotes, which JSON does not allow. The fix is to rescue the specific error whenever the source of the JSON isn’t fully trusted:
require 'json'
malformed = "{'name': 'Ada'}"
begin
data = JSON.parse(malformed)
puts data["name"]
rescue JSON::ParserError
puts "Could not parse JSON input"
end
Output:
Could not parse JSON input
Best Practices
- Always
require 'json'explicitly at the top of any file that uses it, even if another gem happens to load it transitively. - Decide up front whether your code will use string or symbol keys and apply
symbolize_names: trueconsistently, rather than mixing both styles in the same codebase. - Wrap
JSON.parsecalls on external or untrusted input (files, HTTP responses, user-supplied text) inbegin/rescue JSON::ParserErrorinstead of letting a malformed payload crash the program. - Use
JSON.pretty_generatefor files meant to be read or diffed by humans (config files, fixtures); use the compactJSON.generate/to_jsonfor data sent over the wire. - Remember that
symbolize_namesonly affects hash keys — values remain whatever type they were (a JSON string value stays a RubyString, never becomes aSymbol). - For your own classes, define a
to_jsonmethod so instances serialize the way you intend, rather than relying on their default string representation. - Use
data.key?("name")(ordata.key?(:name)with symbolized keys) when you need to distinguish an explicitfalse/nullvalue from a key that’s simply missing.
Practice Exercises
- Given the JSON string
'{"title": "Ruby Basics", "minutes": 45, "tags": ["beginner", "ruby"]}', parse it and print a sentence like"Ruby Basics takes 45 minutes and covers: beginner, ruby". - Build a Ruby array of three hashes representing books (each with
:title,:author, and:year), write it tobooks.jsonusingJSON.pretty_generate, then read the file back and print each title on its own line. - Write a method
safe_parse(json_string)that returns the parsed hash with symbol keys on success, ornilif the string is not valid JSON. Test it against both a valid and an invalid string and print the result of each call.
Summary
require 'json'loads Ruby’s built-in JSON support and addsto_jsonto core classes.JSON.parseconverts a JSON string into RubyHash/Array/scalar values; keys are strings unless you passsymbolize_names: true.JSON.generate,JSON.pretty_generate, andto_jsonconvert Ruby objects back into JSON text; symbols always serialize as strings since JSON has no symbol type.- Malformed JSON raises
JSON::ParserError— always rescue it when parsing input you don’t fully control. - Only
nilandfalseare falsy in Ruby, so usekey?rather than a bare truthiness check when you need to tell an explicitfalse/nullapart from a missing key.
