Question

Parse JSON and Save Nested Values in Ruby on Rails

rubyruby-on-railsjson

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 Hash objects.
  • JSON arrays become Ruby Array objects.
  • JSON strings become Ruby String objects.
  • JSON numbers become Ruby numeric values.
  • JSON true, false, and null become true, false, and nil.

In the example response, shortUrl is nested under two levels:

  1. The top-level results object.
  2. A key whose name is the original long URL.
  3. The shortUrl key 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.parse opens 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.

Take Quiz

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:

  1. body contains JSON text, not a Ruby hash.
  2. long_url holds the key expected inside results.
  3. JSON.parse(body) converts the text into nested Ruby hashes and assigns them to data.
  4. data.dig("results", long_url, "shortUrl") follows the path through the nested hashes.
  5. The final value, "http://bit.ly/1a0p8G", is assigned to short_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.
Take Quiz

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 update when you want to inspect record.errors rather 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

ApproachBest forMissing-key behavior
data["key"]One known hash lookupReturns nil for a missing final key
data["a"]["b"]Trusted nested dataCan raise if data["a"] is nil
data.dig("a", "b")Nested API dataReturns nil if any part is missing
JSON.parse(body)Normal JSON parsingUses string keys by default
JSON.parse(body, symbolize_names: true)Ruby code designed around symbolsUses 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.

Take Quiz

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 dig for nested values that may not exist.
  • Check for nil or blank values before saving.
  • Use update! when a failed save should raise; use update when you will handle false and 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.

Take Quiz

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

Take Quiz

Keep learning

Related questions

View more