HashWithIndifferentAccess: The Rails Convenience That Breaks Your Tests
Here is a test failure that has cost me more time than I want to admit. You assert that a controller returned the params you expected, the two hashes print identically in the failure output, and the test still fails. You reload, you add puts, you start to wonder if the runner is broken. It is not. You are comparing a HashWithIndifferentAccess to a plain Hash, and they are not equal even though they look it.
expected = { name: "Ada", role: "admin" }
actual = params.permit(:name, :role).to_h
assert_equal expected, actual
# Expected: {:name=>"Ada", :role=>"admin"}
# Actual: {"name"=>"Ada", "role"=>"admin"}
The values match. The keys match, sort of. One side has symbol keys and the other has string keys, and that is enough for == to say no. The convenience that made your controller code pleasant just made your test lie to you.
What HashWithIndifferentAccess actually does
HashWithIndifferentAccess is an ActiveSupport class that lets you look up a value with either a string or a symbol and get the same result. It is what params is, it is what you get from session, and it is what request.headers hands back. This is genuinely useful. Web input arrives as strings, but Ruby developers like writing symbols, and HWIA lets both sides stop caring.
h = { name: "Ada" }.with_indifferent_access
h[:name] # => "Ada"
h["name"] # => "Ada"
The trick is that it does not really store both. Internally, every key is converted to a string the moment it goes in. When you read with a symbol, HWIA converts your symbol to a string first, then does the lookup. So this is not a hash that holds symbol and string keys side by side. It is a hash that holds string keys and quietly translates symbols on the way in and out.
That detail is the whole story. The indifference only exists while you go through HWIA’s own methods. The instant you compare it to a plain Hash, or serialize it, or hand it to something that iterates the raw keys, the mask comes off and you are looking at string keys.
Why the comparison fails
Ruby’s Hash#== compares keys with eql?, and a symbol is never eql? to a string. :name and "name" are different objects of different classes. HWIA does not override equality to paper over this, so when you put a plain symbol-keyed hash on one side of assert_equal and a HWIA on the other, the comparison walks the keys, finds :name on the left and "name" on the right, and reports them as different.
{ name: "Ada" } == { name: "Ada" }.with_indifferent_access
# => false
{ "name" => "Ada" } == { name: "Ada" }.with_indifferent_access
# => true
The second line is the tell. Give the plain hash string keys and equality returns true, because now both sides agree on the key type. HWIA was string-keyed the entire time. Your symbol-keyed expectation was the odd one out.
This is why the failure output is so maddening. Minitest prints the two hashes with inspect, and inspect shows :name for a symbol and "name" for a string. If you are skimming, those look close enough that your eye glides right over the quotes. The one character that matters, a quote mark, is the easiest thing in the world to miss at the end of a long day.
The fixes, in order of preference
The cleanest fix is to compare like with like. Decide which representation you care about and normalise both sides to it before asserting.
If you want to assert against symbol keys, pull the HWIA down to a plain hash with symbolized keys:
assert_equal(
{ name: "Ada", role: "admin" },
actual.symbolize_keys
)
If you would rather work in strings, which is often more honest because it matches what actually came over the wire, stringify your expectation instead:
assert_equal(
{ "name" => "Ada", "role" => "admin" },
actual
)
Both work. What you should not do is reach for assert_equal against a raw HWIA with symbol-keyed expectations and hope, because that is the exact mismatch that started this. Pick a side and convert.
There is a subtlety with symbolize_keys worth flagging. It only converts the top level. If your hash is nested, the inner hashes keep their string keys and you are back to a partial mismatch one level down.
{ "user" => { "name" => "Ada" } }.symbolize_keys
# => { user: { "name" => "Ada" } }
The outer key became a symbol, the inner one did not. For nested structures you want deep_symbolize_keys, which recurses:
{ "user" => { "name" => "Ada" } }.deep_symbolize_keys
# => { user: { name: "Ada" } }
The same pairing exists for the other direction, stringify_keys and deep_stringify_keys. When a comparison fails only inside a nested section, a shallow conversion at the top is a common reason. Reach for the deep variant when your data is more than one level deep, which in Rails it almost always is.
Where this bites in real code
Controller tests are the obvious place, but the pattern shows up anywhere HWIA meets a plain hash you built by hand. Comparing a parsed JSON response to a Ruby literal is a classic, because JSON.parse gives you string keys while your expectation is probably written with symbols. Testing a service object that accepts params and returns a transformed hash is another, because the string keys ride along through your code and surface in the result.
Fixtures and factories are a quieter version of the same trap. You define an attributes hash with symbol keys, the code under test round-trips it through something that stringifies the keys, and the assertion at the end fails on a diff that makes no structural sense until you notice the key types diverged somewhere in the middle.
The rule I follow now is simple. The moment a hash has been anywhere near a request, a JSON boundary, or params, I assume it has string keys and I write my assertions in strings. When I control the hash end to end and it never crosses one of those boundaries, symbols are fine. The bugs happen in the seam between the two, where a symbol-keyed literal meets a string-keyed HWIA and == tells the truth even though the output makes it look like a lie.
Seeing the difference
When two hashes should match and do not, and the failure output is two nearly identical blobs, the fastest way to find the culprit is to stop reading character by character and let a tool sort the keys and flag the type difference for you. A symbol key and a string key that print almost the same are exactly the kind of thing a side-by-side diff catches instantly, because it lines the keys up and shows you that :name on the left has no match on the right.
Staring at two hashes that look equal but fail the assertion? Paste them into RubyHash for a clean side-by-side diff that sorts the keys and flags symbol-versus-string mismatches, so the real difference stops hiding behind a quote mark.
Enjoyed this post?
Subscribe to get notified when we publish more Ruby and Rails content.