Why comparing Ruby hashes by eye is so painful
If you write Ruby, you have lived this moment. A test fails, and your terminal prints two hashes that are almost, but not quite, identical:
-{"uid"=>"user@example.test", "active"=>true, "role"=>"admin", "seats"=>3}
+{"uid"=>"user@example.test", "active"=>true, "role"=>"editor", "seats"=>3}
Somewhere in there, one value changed. Your eyes start at the left edge
and crawl across character by character, comparing two strings that were
deliberately built to look the same. With four keys it is annoying. With
forty keys, or hashes nested three levels deep, or keys that come back in
a different order on every run, it becomes genuinely error-prone. You miss
the difference, assume the test is flaky, and lose ten minutes before you
spot that role
quietly went from "admin"
to "editor".
That is exactly the kind of mechanical comparison a computer should be doing for you. RubyHash does it. Paste the two lines and it tells you, instantly and unambiguously, which key changed and what it changed to.
How RubyHash works
The tool runs four steps on whatever you paste, all locally in your browser:
- 1
Parse the Ruby hash syntax
A real parser reads the input into an actual data structure, understanding hashrockets, symbol keys, nesting, and quoting, rather than treating it as a string to be find-and-replaced.
- 2
Convert to JSON and sort the keys
Keys are sorted alphabetically so that ordering differences, which are common when hashes come from database rows or serializers, stop masquerading as real changes.
- 3
Render a side-by-side diff
The two structures are diffed with word-level highlighting, so the single value that actually changed lights up while everything identical stays quiet. Toggle to a unified view for narrow screens.
- 4
Flag type changes
When a field changes type,
nilto a string, an integer to a float, a string to a hash, RubyHash calls it out explicitly. These are the changes most likely to cause a downstream bug and the easiest to miss by eye.
Why a real parser, not a regex
The tempting shortcut is to swap every =>
for a colon, wrap the result in braces, and hand it to a JSON parser. That
works for the simplest hashes and falls apart the moment your data gets
interesting. Real Ruby output is full of cases a naive replace gets wrong:
- → Mixed key styles in one hash:
{:name => "x", "role" => :admin} - → Quoted strings that contain the very characters you are splitting on, like commas, colons, or braces inside a value
- → Deeply nested hashes and arrays, where a single pass of replacements cannot track which brace closes what
- → Ruby literals like
nil,true, andfalsethat must become valid JSON
RubyHash walks the input character by character and understands the grammar of a Ruby hash. That is the difference between a diff you can trust and one that occasionally tears a string in half and lies to you. When the output says nothing else changed, you can believe it.
Who this is for
RubyHash started as a tool for everyday Rails work, where serializer tests, API response assertions, and ActiveRecord attribute comparisons routinely produce hashes too big to eyeball. If any of these sound familiar, the tool will save you time:
- • A Minitest or RSpec assertion failed and dumped two large hashes
- • You are debugging a JSON API and the response does not match what you expected
- • A serializer changed and you need to see exactly which fields moved
- • You are reviewing a pull request that changes a fixture or a config hash
- • You copied a hash out of a Rails console and want it as clean, sorted JSON
Frequently asked questions
What format should I paste in? +
Paste the two lines straight from your test runner, including the leading - and + that Minitest adds to the expected and actual values. RubyHash also accepts a single hash on its own, or two hashes on separate lines without the diff markers.
Does my data get sent anywhere? +
No. All parsing and diffing happens in your browser with JavaScript. The hashes you paste are never uploaded, logged, or stored on a server. You can disconnect from the internet after the page loads and the tool still works.
Does it handle nested hashes and arrays? +
Yes. The parser is a hand-written recursive descent parser, so it understands hashes inside arrays inside hashes to any depth, along with mixed hashrocket and symbol syntax in the same structure.
What about nil, symbols, and other Ruby types? +
nil becomes null, symbols are preserved as readable keys, and true/false map to JSON booleans. When a value changes type between the two hashes, for example nil to a string or an integer to a float, RubyHash flags it explicitly instead of letting it slip by.
Is RubyHash free? +
Yes, completely free, with no sign-up and no installation. It runs entirely in the browser. The site is supported by unobtrusive ads and an optional newsletter.
From the blog
View all posts →Alongside the tool, the blog covers the Ruby and Rails topics that come up while writing and debugging this kind of code: testing, hashes, pattern matching, value objects, and more.
deep_merge in Ruby: Why merge Clobbers Your Nested Hashes
Hash#merge is shallow, so merging nested hashes silently throws away the nested data you meant to keep. Here is why it happens and how deep_merge fixes it.
Comparing JSON Responses in Rails Tests Without Losing Your Mind
Asserting that a Rails JSON API returns the right body sounds simple, until the test fails on two blobs that look identical. Here is what actually trips these assertions and how to compare responses cleanly.
Why Timestamps Break Hash Equality in Ruby Tests
Your test asserts two hashes are equal, the timestamps look identical, but it still fails. Here is why Ruby time values break hash equality and how to fix it.
Symbol vs String Keys: The Hash Bug That Hides in Plain Sight
In Ruby, :name and "name" are different hash keys, even though they look almost the same. That one distinction is behind a whole category of nil-returning, test-failing bugs.