Skip to main content

JSON to Ruby Hash: Symbol Keys, Rocket Arrows, and Rails Fixtures

Turn API JSON into an idiomatic Ruby Hash literal with symbol keys, => rocket syntax, or string keys. Worked examples for Rails fixtures and nested structures.

Published By Li Lei
#ruby #json #rails #developer-tools

JSON to Ruby Hash: Symbol Keys, Rocket Arrows, and Rails Fixtures

Most of my Ruby debugging starts with a blob of JSON I copied out of a log line. I need it as a real Ruby value so I can poke at it in pry, drop it into a spec, or seed a database. Retyping braces by hand is how you introduce a nil you didn't expect at 11pm. The faster path is to paste the JSON into /en/t/json-to-ruby/ and get a Ruby Hash literal back, then decide which key style fits the file it's going into.

This post walks through that decision: when to use symbol keys, when the rocket arrow => earns its keep, how nested structures map, and how the same converter feeds Rails fixtures and seed data.

Three key styles, three different jobs

Ruby gives you more than one way to write a Hash key, and the choice is not cosmetic.

  • Symbol shorthandname: value. This is the everyday form. Symbols are interned once, they compare fast, and Rails params, config hashes, and most gems hand you symbol keys by default. If the Hash lives inside your own Ruby code, this is almost always right.
  • Rocket arrows:name => value. Same symbol key, older syntax. You need it the moment a key is not a legal Ruby label, because first-name: 1 does not parse but :"first-name" => 1 does. The arrow form is also what you reach for when a key starts with a digit or contains a space.
  • String keys"name" => value. Use these when the Hash mirrors external JSON you will re-serialize, or when a key could collide with a Ruby reserved word. String keys keep the data honest about being foreign.

A concrete point worth memorizing: the shorthand label form name: is only valid for keys that are legal Ruby labels. A key like first-name cannot become a bare label, so the converter falls back to a quoted symbol or the rocket arrow form rather than emitting code that fails to parse. Don't hand-edit it back to a bare label — it won't load.

A worked example: GitHub user JSON to a Ruby Hash

Here is a trimmed API response and what it becomes with symbol shorthand keys.

Input JSON:

{
  "id": 1,
  "login": "octocat",
  "company": null,
  "plan": {
    "name": "pro",
    "private_repos": 9999
  },
  "emails": ["octocat@github.com"]
}

Output Ruby Hash:

{
  id: 1,
  login: "octocat",
  company: nil,
  plan: {
    name: "pro",
    private_repos: 9999
  },
  emails: ["octocat@github.com"]
}

Notice three things. JSON null became Ruby nil, because nil is Ruby's single representation of absence the way null is JSON's. The integers passed through verbatim. And the nested plan object rendered as a nested Hash literal, indented two spaces per level, so the whole thing drops into a .rb file or an irb session with no reshaping.

If the source had dashed keys, the same payload with rocket arrows would read :"private-repos" => 9999 instead of a bare label — same data, syntax that actually parses.

Nested objects and arrays of objects

Real payloads are not flat, and the mapping has to hold the whole tree together.

In Hash mode a nested object stays a nested Hash and a JSON array stays a Ruby array, so the structure you paste is the structure you get. An array of objects keeps each element as its own Hash inside a Ruby array — exactly what you want for seed data.

One detail that trips people up: when one record in an array has a key another record lacks, the missing key renders explicitly as nil rather than vanishing. So [{"id":"a"},{"id":"b","note":"x"}] gives you two hashes where the first carries note: nil. Every element ends up with the same attribute set, which matters the moment you loop over them and call the same create! on each.

Seeding a Rails fixture from a captured payload

This is the case I hit most often. You captured an API response, and you need it as a Ruby Hash inside a spec or a factory.

Paste the JSON, keep symbol keys in shorthand form, and copy the output straight into a let block or a FactoryBot trait:

let(:webhook_payload) do
  {
    id: 1,
    login: "octocat",
    company: nil,
    plan: { name: "pro", private_repos: 9999 }
  }
end

The null fields come out as nil, the nested objects come out as nested hashes, and the fixture matches the wire shape exactly — without you retyping a single brace. If the data is a JSON array of reference rows, stay in Hash mode and you get a Ruby array of hashes ready for Model.create!(attrs) in seeds.rb.

A caution about db:seed: if you also want attribute readers and a fixed shape instead of h["server"]["port"] chains, the converter's Struct mode emits Struct.new class definitions and the call that fills them. With keyword_init on, positional order stops mattering. With it off, the new call binds arguments by position, so reordering JSON keys silently reassigns values — keep keyword_init on whenever the source key order is not stable.

Why a literal beats parsing at runtime

You could always ship the JSON as a string and call JSON.parse at runtime. Sometimes that is right. But a Ruby literal has real advantages for fixtures, seeds, and console work:

  • It evaluates on the spot in pry or irb, so you can immediately call .dig, .map, and .select on it instead of squinting at raw text.
  • It carries no parse step and no require "json", so a spec stays self-contained.
  • With symbol keys it reads the way the rest of your Ruby reads, instead of forcing string-key lookups that return nil on a typo.

Everything runs in your browser, so the JSON you paste never touches a server — useful when the payload came from a private API. If you want to clean up or pretty-print the raw JSON before converting, run it through /en/t/json-formatter/ first, then bring the formatted text over.

Same input, other targets

If your stack is polyglot, the same JSON often needs to land in more than one language. The converter family shares the null-to-empty and nested-tree handling, so the mental model carries across:

Pick the key style for where the code lives — symbol shorthand inside Ruby, rocket arrows when a key won't make a label, string keys when you're mirroring foreign JSON — and the rest is paste, copy, ship.


Made by Toolora · Updated 2026-06-13