Question
How can I parse a JSON response in Ruby on Rails, extract the nested shortUrl value returned by the Bitly API, and save it to an Active Record object associated with the original long URL?
For example, given this JSON response:
{
"errorCode": 0,
"errorMessage": "",
"results": {
"http://www.foo.com": {
"hash": "e5TEd",
"shortKeywordUrl": "",
"shortUrl": "http://bit.ly/1a0p8G",
"userHash": "1a0p8G"
}
},
"statusCode": "OK"
}
How do I retrieve http://bit.ly/1a0p8G and store it in the relevant Rails model record?
Short Answer
By the end of this page, you will be able to convert JSON text into Ruby hashes, navigate nested API data, handle dynamic keys such as a long URL, and save the extracted value using Active Record.
Concept
JSON (JavaScript Object Notation) is a text format commonly returned by web APIs. Although JSON looks similar to a Ruby hash, it is initially just a string when received in an HTTP response.
Ruby cannot access a JSON string with hash syntax until you parse it:
require "json"
data = JSON.parse(json_string)
JSON.parse converts JSON text into Ruby objects:
- JSON objects become Ruby
Hashobjects. - JSON arrays become Ruby
Arrayobjects. - JSON strings become Ruby
Stringobjects. - JSON numbers become Ruby numeric values.
- JSON
true,false, andnullbecometrue,false, andnil.
In the example response, shortUrl is nested under two levels:
- The top-level
resultsobject. - A key whose name is the original long URL.
- The
shortUrlkey in that URL's result object.
Parsing and navigating API responses matters because external data is often nested, may be incomplete, and should be validated before it changes your database.
Mental Model
Think of a JSON response as a set of labelled storage boxes delivered as a sealed package.
- The HTTP response body is the sealed package: it is text.
JSON.parseopens the package and turns it into Ruby boxes you can inspect.- Each hash key is a label on a box.
- Nested hashes are boxes inside other boxes.
For this response, you first open results, then find the box labelled with the long URL, and finally retrieve the item labelled shortUrl.
Syntax and Examples
Parse a JSON string with Ruby's built-in JSON module:
require "json"
json = <<~JSON
{
"statusCode": "OK",
"results": {
"http://www.foo.com": {
"shortUrl": "http://bit.ly/1a0p8G"
}
}
}
JSON
data = JSON.parse(json)
After parsing, data is a Ruby hash. JSON object keys become string keys by default:
puts data["statusCode"]
# => OK
To access a nested value, use one hash lookup per nesting level:
long_url = "http://www.foo.com"
short_url = data["results"][long_url]["shortUrl"]
puts short_url
# => http://bit.ly/1a0p8G
The long URL is not merely an example value here; it is the actual key inside results. Storing it in long_url makes the lookup clear and works for whichever URL was sent to the API.
In Rails, JSON is normally already available. If you use plain Ruby, add require "json".
Step by Step Execution
Consider this response body:
body = '{"results":{"http://www.foo.com":{"shortUrl":"http://bit.ly/1a0p8G"}}}'
long_url = "http://www.foo.com"
data = JSON.parse(body)
short_url = data.dig("results", long_url, "shortUrl")
Execution trace:
bodycontains JSON text, not a Ruby hash.long_urlholds the key expected insideresults.JSON.parse(body)converts the text into nested Ruby hashes and assigns them todata.data.dig("results", long_url, "shortUrl")follows the path through the nested hashes.- The final value,
"http://bit.ly/1a0p8G", is assigned toshort_url.
dig is especially useful for API data because it returns nil if an intermediate key is missing instead of raising an error.
missing = data.dig("results", "http://missing.example", "shortUrl")
# => nil
Real World Use Cases
JSON parsing is used whenever a Rails application receives structured data from outside its own models.
- URL-shortening services: Read a shortened URL and save it alongside the original URL.
- Payment providers: Extract a payment ID, status, or receipt URL from an API response.
- Weather and mapping APIs: Read temperature, coordinates, addresses, or route details.
- Webhooks: Parse incoming JSON to respond to events such as completed orders or new subscriptions.
- Background jobs: Fetch API data on a schedule and update local records.
- Data imports: Read JSON files and create or update records in bulk.
Real Codebase Usage
In production Rails code, JSON parsing is usually kept outside controllers when it involves an external service. A service object can request the API, parse the response, validate it, and update the model.
Use dig and guard clauses to make missing data explicit:
short_url = payload.dig("results", url_record.long_url, "shortUrl")
return unless short_url.present?
url_record.update!(short_url: short_url)
Common project patterns include:
- Service objects: Put API request and response handling in a class such as
UrlShortener. - Bang methods for required writes: Use
update!when a failed database update should raise an exception and be handled by the job or caller. - Non-bang methods for expected failures: Use
updatewhen you want to inspectrecord.errorsrather than raise. - Background jobs: Make network calls outside the web request when the API may be slow.
- Validation before persistence: Check the API status and ensure the extracted value is present.
- Logging: Record unexpected payloads carefully, without logging secrets or private user data.
A focused service object might look like this:
class ShortUrlSaver
def self.call(url_record, response_body)
payload = JSON.parse(response_body)
short_url = payload.dig("results", url_record.long_url, "shortUrl")
return false if short_url.blank?
url_record.update(short_url: short_url)
rescue JSON::ParserError
false
end
end
Common Mistakes
Trying to index JSON text directly
This does not parse the response:
body = '{"statusCode":"OK"}'
body["statusCode"]
body is a string, so parse it first:
data = JSON.parse(body)
data["statusCode"]
Using symbols when parsed keys are strings
By default, JSON.parse produces string keys:
data[:results] # => nil
data["results"] # => { ... }
Use string keys consistently, or deliberately parse with symbolized names when appropriate:
data = JSON.parse(body, symbolize_names: true)
data[:results]
For external API payloads, string keys are often clearer because JSON itself uses strings.
Assuming the long URL is always a fixed key
This only works for one exact URL:
data["results"]["http://www.foo.com"]["shortUrl"]
Use the record's actual URL instead:
data.dig("results", url_record.long_url, "shortUrl")
Chaining [] when data may be absent
This can raise NoMethodError if results or the URL entry is missing:
data["results"][long_url]["shortUrl"]
Prefer dig for optional API fields:
data.dig("results", long_url, "shortUrl")
Saving without checking the API result
A successful HTTP request does not always mean the payload contains usable data. Check the expected status and extracted field before updating your record.
Rescuing every error silently
Avoid this:
rescue StandardError
nil
end
Rescue specific expected errors, such as JSON::ParserError, and log or report unexpected failures.
Comparisons
| Approach | Best for | Missing-key behavior |
|---|---|---|
data["key"] | One known hash lookup | Returns nil for a missing final key |
data["a"]["b"] | Trusted nested data | Can raise if data["a"] is nil |
data.dig("a", "b") | Nested API data | Returns nil if any part is missing |
JSON.parse(body) | Normal JSON parsing | Uses string keys by default |
JSON.parse(body, symbolize_names: true) | Ruby code designed around symbols | Uses symbols recursively; use deliberately |
dig does not replace validation. It prevents a lookup error, but you should still decide what to do when it returns nil.
Cheat Sheet
# Parse JSON text into Ruby hashes and arrays
payload = JSON.parse(response_body)
# Read a top-level JSON value
status = payload["statusCode"]
# Read a known nested value
short_url = payload["results"][long_url]["shortUrl"]
# Safer nested lookup: returns nil for missing levels
short_url = payload.dig("results", long_url, "shortUrl")
# Save to an Active Record model
url_record.update!(short_url: short_url)
# Handle invalid JSON
begin
payload = JSON.parse(response_body)
rescue JSON::ParserError => error
Rails.logger.warn("Invalid JSON: #{error.message}")
end
- JSON keys are strings by default after
JSON.parse. - Use the actual dynamic key, such as
url_record.long_url. - Use
digfor nested values that may not exist. - Check for
nilor blank values before saving. - Use
update!when a failed save should raise; useupdatewhen you will handlefalseand validation errors.
FAQ
How do I parse JSON in Ruby on Rails?
Call JSON.parse with the JSON string or HTTP response body:
payload = JSON.parse(response.body)
Why does payload[:results] return nil?
JSON.parse creates hashes with string keys by default. Use payload["results"], or parse with symbolize_names: true if symbol keys are intentional.
How do I access JSON with a dynamic key in Ruby?
Store the dynamic key in a variable and use it in the lookup:
payload.dig("results", long_url, "shortUrl")
What does Hash#dig do in Ruby?
It walks through nested hashes or arrays. If a step is missing, it returns nil instead of failing due to a later lookup on nil.
How do I save a parsed JSON value to a Rails model?
After extracting and validating the value, call update or update!:
record.update!(short_url: short_url)
What error is raised for invalid JSON in Ruby?
JSON.parse raises JSON::ParserError when the supplied text is not valid JSON.
Should I parse JSON in a Rails controller?
For small request payloads, Rails often parses JSON request bodies automatically. For external API responses, parsing is commonly placed in a service object or background job so controllers remain focused on HTTP flow.
Mini Project
Description
Build a small Rails-oriented URL result saver. It receives an API response body and a URL record, extracts the shortened URL from nested JSON, and updates the record only when a usable value exists. This mirrors a common integration task: synchronizing data from an external API into your application database.
Goal
Create a method that safely parses a URL-shortener response and saves its shortUrl value to an Active Record record.
Requirements
Requirement 1
Keep learning
Related questions
Add a Default Value to an Existing Column in a Rails Migration
Learn how to add or change a default value on an existing database column using Rails Active Record migrations safely.
Calling a Class Method from an Instance in Ruby
Learn how to call a class method from an instance in Ruby using self.class, with examples, pitfalls, and practical usage patterns.
Calling an Overridden Monkey-Patched Method in Ruby
Learn how to call the original method when monkey patching in Ruby, including alias_method patterns, examples, pitfalls, and practical usage.