JSON.parse hands you string keys unless you pass symbolize_names: true, and even then a round trip through JSON is not lossless. Here is what the option does, what it cannot restore, and how to compare parsed JSON in tests.
transform_keys and transform_values are the cleanest way to reshape a Ruby hash, but they are shallow, they drop defaults, and they can collapse keys without warning. Here is how they behave and where they bite.
Calling sort on a Ruby hash hands you an array of pairs, not a hash. Here is why that happens, how to get a sorted hash back, and the cases where sorting actually buys you something.
Comparing two arrays of hashes with == fails the moment the order changes, even when the data is identical. Here is why it happens and the patterns that actually compare the content.
RSpec diffs two hashes by pretty printing both and comparing the text, which is why the output looks like noise. Here is what actually causes it and the ways to get a readable answer.
ActionController::Parameters is not a Hash, and to_h behaves differently depending on whether you permitted anything. Here is what actually comes back and why your test fails on a hash that looks right.
Hash#slice keeps the keys you name, except drops them. Reaching for either instead of hand-rolled select or delete_if removes a whole category of small bugs.
A single-line inspect of a large Ruby hash is unreadable. Here are the built-in and gem-based ways to pretty print a hash, and when each one is the right call.
HashWithIndifferentAccess lets you read a key with a string or a symbol, until you compare it to a plain hash in a test and everything falls apart. Here is why it happens and how to compare cleanly.
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.
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.
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.
When a Minitest assertion fails on two hashes, the output is nearly unreadable. Here's why it happens and a faster way to find the one value that changed.
Several ways to compare two Ruby hashes and find exactly what changed, from plain Ruby techniques to a visual side-by-side diff, with the tradeoffs of each.
Your Rails test asserts two hashes are equal, they look identical, but the test still fails. Here's a checklist of the usual causes and how to find the real one.
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.
Ruby has four different methods for asking whether two things are equal, and they don't mean the same thing. Knowing which is which explains case statements, hash keys, and a class of subtle bugs.
Ruby 3.2 added Data.define, a built-in way to create small immutable value objects. It's what Struct should have been, and it replaces a surprising amount of boilerplate.
Both reduce and each_with_object fold a collection into a single result, but they disagree about who owns the accumulator. Getting that wrong is the most common Enumerable bug there is.
The ||= memoization pattern is everywhere in Ruby code, and it quietly breaks on nil, false, and any method that takes arguments. Here is what actually happens and how to do it right.
Ruby hashes can carry their own default value or default block. Used well, they replace nil checks, initialization ceremony, and a surprising amount of glue code.
Ruby's lazy enumerators let you chain map, select, and reject on enormous datasets without loading everything into memory. Here's how they work and when to reach for them.
Most Ruby error handling is either too broad or too silent. Here's how to use rescue, retry, and custom exceptions to build code that fails clearly and recovers gracefully.
Ruby's Enumerable module gives you 50+ methods for free. Understanding it changes how you think about collections, pipelines, and data transformation.
Hash#[] silently returns nil for missing keys. Hash#fetch raises an error. That distinction prevents more bugs than you'd expect.
Ruby has three methods for building clean data pipelines: tap, then, and yield_self. Each does something slightly different, and knowing which to reach for makes your code more expressive.
Why Ruby's 'if it quacks like a duck' philosophy leads to more flexible, testable, and elegant code.
Ruby's metaprogramming is its most powerful and most dangerous feature. Here's how experienced teams decide when to use it and when to walk away.
Concerns are controversial in the Rails world. They shouldn't be. The problem isn't the pattern, it's how people use it. Here's how to use concerns well.
Ruby was designed to make programmers happy. That's not a slogan, it's a design philosophy that shaped how the language reads, and why that still matters.
When you call a method in Ruby, there's a specific chain of places Ruby looks to find it. Understanding that chain is the key to debugging 'wrong method called' bugs.
The frozen_string_literal pragma is one line of code that prevents mutation bugs, improves performance, and signals intent. Here's why you should be using it everywhere.
Ruby didn't just popularize test-driven development, it made testing feel inevitable. Here's how a community obsession became an industry standard.
Most Rake tasks are untested blobs of procedural code. Here's how to write ones that are namespaced, documented, testable, and actually maintainable.
Include Comparable, define one method, and get six comparison operators plus sorting for free. It's one of Ruby's best-kept secrets for custom objects.
N+1 queries, unnecessary column loading, and the count/size/length trap. These three patterns silently kill Rails performance, and they're easy to fix.
Ruby 3's pattern matching with case/in isn't just syntax sugar. It's a fundamentally different way to destructure data, and it's been production-ready since 3.1.
Ruby gives you two ways to create simple data objects. One is fast, explicit, and production-ready. The other is convenient, flexible, and almost always the wrong choice.
Introducing RubyHash, a tool that turns unreadable Minitest hash diffs into clean, sorted, side-by-side comparisons so you can see exactly what changed.
Subscribe to get new Ruby and Rails articles delivered straight to your inbox.