# super_diff: an RSpec differ that understands nested hashes instead of printing them

> super_diff replaces RSpec's string based diffing with an engine that walks two data structures and shows where they actually differ, which matters when a matcher fails on API or database payloads. It is MIT licensed, supports Ruby 3.1 and RSpec 3.x, and extends RSpec rather than replacing it.

**splitwise/super_diff** — A more helpful way to view differences between complex data structures in RSpec.

- Repository: https://github.com/splitwise/super_diff
- Website: https://splitwise.github.io/super_diff/
- Stars: 1,048 · Forks: 65
- Language: Ruby
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/splitwise-super-diff

## Why RSpec's own diff fails on anything that is not a string

The primary motivation behind the gem is stated in one line, to vastly improve upon RSpec's built-in diffing capabilities.

The mechanism of the problem is specific, and the README links to the exact RSpec source that causes it. Whenever you use a matcher such as eq, match, include or have_attributes, you get a diff of the two data structures. That is great if all you want to do is compare multi-line strings. But if you want to compare other, more real world kinds of values such as API or database data, then you are out of luck.

The reason is that RSpec merely runs your expected and actual values through Ruby's PrettyPrinter library and then performs a diff of these strings. The output it produces leaves much to be desired.

That is the whole diagnosis, and it is worth internalising because it explains both the value and the limits of what follows. RSpec is not doing anything wrong. It is a string differ applied to structures, and it does the best a string differ can do. What is missing is a differ that knows a hash has keys, that an array has elements in order, and that two of those elements may themselves be hashes.

So the failure output you get is not a bug in RSpec, it is the shape of a design decision made when the expectation was a string. And the fix is not a better printer, it is a different model of what a diff is.

## The example that makes the case, with a nested hash and a list of items

The README walks through a comparison that contains exactly the kinds of value that defeat a string differ: a nested hash, a custom object, a nested hash of addresses, and an array of hashes with differing keys.

The actual value has a customer with a person object, a shipping address, and an array of items.

```ruby
actual = {
  customer: {
    person: SuperDiff::Test::Person.new(name: "Marty McFly, Jr.", age: 17),
    shipping_address: {
      line_1: "456 Ponderosa Ct.",
      city: "Hill Valley",
      state: "CA",
      zip: "90382"
    }
  },
  items: [
    { name: "Fender Stratocaster", cost: 100_000, options: %w[red blue green] },
    { name: "Mattel Hoverboard" }
  ]
}
```

The expected value differs in three places, and each difference is a different kind. The person's name is Marty McFly, Jr. in the actual and Marty McFly in the expected, a difference inside a custom object. The shipping address line_1 is a different street address, a difference inside a nested hash. And the items array has Mattel Hoverboard in one and Chevy 4x4 in the other, a difference inside an array of hashes where one entry has keys the other lacks.

A string differ sees three changed regions in a pretty printed blob. A structure aware engine sees a changed attribute, a changed value at a path, and a changed array element with an added and a removed key.

The README shows both outputs as screenshots, before super_diff and after, and the pair is the most persuasive thing in the project documentation. The text version of the claim is that the library provides a diff engine that knows how to figure out the differences between any two data structures and display them in a sensible way.

## What the diff engine actually models, and where the inspectors come from

The engine walks the two structures in parallel and reports the differences it finds, which means it has to decide how to inspect each kind of value. That decision is the interesting part of the design, and the credits section explains where the approach came from.

The library started with Diff::LCS, which the author says is the library he started with in the original version of this gem, made in 2011. The pretty printing algorithms and API within PrettyPrinter and AwesomePrint were borrowed from, and the inspectors were developed from those ideas, with the inspection tree living in lib/super_diff/core/inspection_tree.rb.

So the architecture is a tree of inspectors, one per kind of value, with a diff engine that walks both trees and reports the divergences it finds. That is why it can handle a custom object like SuperDiff::Test::Person and a hash and an array in the same comparison without special casing, and it is also why the project calls the objects inspectors rather than adapters or matchers.

The practical consequence for a user is that the output is structured around the shape of the data rather than around a diff format. A difference is reported as a path into the structure with the values on either side, and for a custom object the engine needs to know how to compare its attributes, which is what the object in the example is for.

One thing the README does not spell out is how the engine handles a class that does not expose its internals, or a value that is not comparable at all. That is the kind of question the user documentation at the project site covers, and it is worth reading before you point it at an arbitrary payload.

## Adding it to a project, and the binary string case that needs its own require

The installation and usage details are in the user documentation on the project site, and the README itself only points there rather than repeating the steps.

The one piece of code the README does show is the optional extension. If you need diffs for binary strings, with an Encoding of ASCII_8BIT, you require the binary string integration.

```ruby
require "super_diff/binary_string"
```

What it does is enable hex dump diffs and keep binary data out of the expectation text. That is a small feature and a revealing one, because it shows the failure mode of a structure aware differ. Once the engine is walking your data rather than printing it, a value that is not printable text has to be rendered some other way, and a hex dump is the conventional answer. Without the extension, those bytes end up in the expectation text as something unreadable.

The compatibility section gives the version floor the project tests against, and it is a small set of constraints.

```
`super_diff` is tested to work with
Ruby >= 3.1,
RSpec 3.x,
and Rails >= 7.0.
```

Ruby 3.1 as a floor is a modern baseline, and the RSpec 3.x constraint tells you the integration surface, since super_diff extends RSpec's differ rather than offering a separate API. The Rails line is about the ActiveSupport and RSpec Rails integration in a typical application, and the project states it as a tested combination rather than a dependency.

The repository layout also shows how the gem is built and documented. There is a .rubocop.yml with a .rubocop_todo.yml beside it, a .husky/ directory and a yarn/ directory with a .yarnrc.yml, a package.json pinning yarn at 3.4.1 with husky, a pyproject.toml that configures poetry for the documentation, and an mkdocs.yml at the root. The docs are written in Markdown, built with mkdocs and mkdocs-material, and the pyproject.toml carries a dev group with mkdocs and mkdocs-material. The .python-version file and the poetry setup exist only for the documentation toolchain, which is an unusual and slightly awkward but honest arrangement: a Ruby gem whose website is built by a Python tool.

## What the diff will not tell you, and the alternative worth naming

Two limits, and the second is a design consequence rather than a shortcoming.

The first is scope. This is an RSpec differ. It is not a general purpose diffing library for arbitrary Ruby objects, and it is not a replacement for a diff tool in a code review. The README is careful about the framing, describing the gem as designed to display the differences between two objects of any type in a familiar and intelligent fashion, and the whole project is oriented around the matcher failure output.

The second is what a structural diff fundamentally cannot tell you. It tells you that a value differs. It does not tell you why the value is different, whether the difference is the bug, or whether the expected value was the one that was wrong. A structure aware engine gives you a better location for a difference, and that is genuinely valuable when the difference is a missing key in a nested payload. It does not turn a failing test into a diagnosis, and a team whose problem is that the tests are wrong rather than the code is not going to be helped much by a prettier report.

The real alternative is to compare less. A test that asserts on the whole response hash will always fail with a large diff, whatever the differ, because the diff has to show the whole shape. Tests that assert on specific keys, on a length, or on a property fail with a small diff and read clearly in any differ. That approach costs more test code and it duplicates knowledge of the payload in the assertions, which is why people reach for a whole object comparison in the first place.

So the comparison is this. super_diff makes the whole object comparison readable, which is a real improvement over string diffing. Writing the assertions more narrowly makes the failure obvious without any differ at all, at the price of a larger test suite. For a payload that rarely changes shape, the second is better. For one that changes often, the first saves a great deal of typing.

## Conclusion

super_diff fits a Ruby team whose RSpec failures print walls of pretty printed text when a nested hash or an array of hashes does not match, and who want the failure to point at the differing key rather than at the whole structure. It does not fit a project that is not already on RSpec, and it does not fit a team that wants a general purpose diffing library for arbitrary Ruby objects outside a test run. Check three things before you adopt it. Check your Ruby and RSpec versions, because the gem is tested to work with Ruby >= 3.1, RSpec 3.x and Rails >= 7.0, and it is an RSpec integration rather than a standalone tool. Check how it hooks into RSpec, because the value depends on it replacing the differ rather than on you calling it directly. And check whether your payloads contain binary strings, since that needs a separate require of the binary string integration. The last push was on 2026-09-18, the newest release listed is v0.19.0 from 2026-05-01, and the gem is MIT licensed and maintained by Splitwise.

## FAQ

### What does the super_diff gem do?

It provides a diff engine that knows how to figure out the differences between any two data structures and display them in a sensible way, replacing RSpec's practice of running expected and actual values through Ruby's PrettyPrinter and diffing the resulting strings.

### Which versions of Ruby and RSpec does super_diff support?

The project states that super_diff is tested to work with Ruby >= 3.1, RSpec 3.x, and Rails >= 7.0. It is an RSpec integration rather than a standalone diffing library.

### How do I get hex dump diffs for binary strings in super_diff?

Require the binary string integration with require "super_diff/binary_string" for strings with an Encoding of ASCII_8BIT. It enables hex dump diffs and keeps binary data out of the expectation text.

### Who maintains super_diff and under what license?

SuperDiff was created by Elliot Winkler and is maintained by Splitwise, Inc, and it is released under the MIT license with a LICENSE file at the root of the repository.

### Which libraries did super_diff take inspiration from?

Diff::LCS, which the author started with in the original version of the gem made in 2011, and the pretty printing algorithms and API within PrettyPrinter and AwesomePrint, from which the inspectors were developed.

### How current is the super_diff gem?

The newest listed release is v0.19.0 from 2026-05-01, preceded by v0.18.0 on the same day and v0.17.0 from 2025-10-24. The last push to the repository was on 2026-09-18, and the project keeps a CHANGELOG.md with what changed in recent versions.

## Sources

- [License: MIT](https://github.com/splitwise/super_diff/blob/main/LICENSE)
- [Project website](https://splitwise.github.io/super_diff/)
- [README](https://github.com/splitwise/super_diff/blob/main/README.md)
- [Releases](https://github.com/splitwise/super_diff/releases)
- [splitwise/super_diff on GitHub](https://github.com/splitwise/super_diff)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/splitwise-super-diff
