matcher-combinators: assertions about nested Clojure data
Library for creating matcher combinator to compare nested data structures
At a glance
- What is it?
- matcher-combinators is a small Clojure library that gives you composable matchers for asserting things about nested data structures, so a test can say what shape it expects rather than comparing whole maps. A plain value becomes an equals matcher, a map becomes an embeds matcher that ignores the keys you did not mention, and a failed assertion prints a diff of what actually arrived.
- Who is it for?
- matcher-combinators suits a Clojure codebase whose tests compare whole maps and fail with unhelpful output, and it suits teams that want assertions to state intent rather than reproduce the expected value. It does not suit a codebase that does not use Clojure, and it does not suit assertions over large sets or long sequences, because the README is explicit that those operations are factorial in the number of elements.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 28 days ago.
- What is it written in?
- Mainly Clojure, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem: testing what data looks like
matcher-combinators is a library for making assertions about nested data structures, and the motivation section explains the gap it fills better than the feature list does.
Clojure's built-in data structures get you a long way when trying to codify and solve difficult problems, and a solid selection of core functions lets you create and access core data structures easily. But that flexibility does not extend to testing: the README says we seem to be missing a complete yet extensible way to assert that the data fits a particular structure.
So the library provides composable matcher combinators that can be used as building blocks to test functions that evaluate to nested data structures more effectively.
That is the whole problem statement, and it is a specific Clojure problem. In a language where a function returns a map with eight keys and you only care about two of them, the natural test is to write the whole expected map, which means every unrelated change to the returned data breaks a test that was never about that key.
The feature list is short: matchers for scalar and structural values, good readability supported by default interpretations of Clojure types as matchers, pretty-printed diffs when the actual result does not match the expected matcher, and integration with clojure.test and midje.
The project is maintained by the Devflow team at Nubank, with Phillip Mates as a steward, and questions go to the Clojurians Slack channel. The library is on Clojars as nubank/matcher-combinators, the docs are on cljdoc, the stated Clojure version compatibility is 1.8 and up, and there is a badge for Babashka compatibility.
Default interpretations, and why a map is not an equals matcher
The default matcher behaviour is the part that makes the library usable without ceremony.
When an expected value is not wrapped in a specific matcher, the default interpretation is: all scalar and collection types except regex and maps are equals, a regex is a regex matcher, and a map is embeds.
The map rule is the important one, and it changes what a test means. A map is interpreted as an embeds matcher, which ignores unspecified keys. So an expected map of one key asserts that one key has that value and says nothing about the rest of the map.
That is the opposite of what equals would do, and it is the reason the library exists. A test asserting on the name field of a returned map no longer breaks when the map gains a new field.
The rest of the defaults follow the type of the value. A sequence is an equals matcher, which specifies the count and the order of the matching elements, and the elements themselves are matched based on their types, so a sequence of numbers is compared exactly while a sequence of predicates is applied element by element. A set is also an equals matcher, and the same idea applies to a map with a different comparison function.
If you want to know which matcher would be used for a given value, there is a function for it. matcher-for discovers it, and the README shows it returning the embeds function for a map value.
So the rule of thumb is simple: a value means what it looks like, except that a map is a subset assertion.
The combinators, from prefix to via
The built-in matchers cover the cases where the default interpretation is not what you want, and the README lists them with their semantics.
equals operates over any scalar value or collection. For a scalar it matches when the given value is exactly the same as the expected, and for a map it matches when the keys of the expected map are equal to the given one and the values match.
prefix handles the case where you only care about the first n items of a sequence. in-any-order handles the case where order does not matter, and its cost is stated plainly in the README: it is O(n!) because it compares every expected element with every actual element in order to find a best match for each one, removing matched elements from both sequences as it goes. The advice is to avoid applying it to long sequences.
set-equals repeats predicates for sets, with the same warning, since matching sets is also an O(n!) operation for the same reason.
There is also via, which transforms the actual value before matching. The README's example applies read-string to the actual value, which is a string, before matching it against a map, and the way it is written in the test is as a matcher inside the expected structure, in the payloads key of the expected map.
The naming is a little confusing at first because via appears in the position of a matcher within the expected value, but the behaviour is the useful one: it says this particular part of the structure needs decoding before it is compared.
So the combinators cover three kinds of looseness: a prefix of a sequence, an unordered comparison, and a transformation of one value.
clojure.test integration, and the Midje deprecation
The integration with clojure.test is the part you will use most, and it is a small extension rather than a new framework.
You require the matcher-combinators.test namespace, which extends clojure.test's is macro to accept the match? and thrown-match? directives. match? takes the matcher combinator representing the expected value as its first argument and the expression being checked as the second.
thrown-match? is the exception case, and it takes three things: a throwable subclass, a matcher combinator to be applied to the ex-data of the exception thrown, and the expression being checked. There is also a two-arity version that takes just the matcher combinator and the expression, so you can assert on the shape of the exception data without naming the exception type.
So a normal assertion looks like this:
(is (match? (m/equals 37) (+ 29 8)))
(is (match? (m/regex #"fox") "The quick brown fox jumps over the lazy dog"))and an exception assertion looks like this:
(is (thrown-match? clojure.lang.ExceptionInfo
{:foo 1}
(throw (ex-info "Boom!" {:foo 1 :bar 2}))))The Midje situation is worth knowing about before you plan around it. Support for Midje has been deprecated in matcher-combinators. The matcher-combinators.midje namespace continues to ship to avoid breaking changes, but midje is no longer included as a transitive dependency, which means your project has to declare it itself if you use that path.
There is also a standalone namespace, matcher-combinators.standalone, which provides an API for using matcher-combinators outside the context of a test framework, for example in a validation function in application code.
The repository: Clojure, Babashka and a release process
The repository layout shows a project with a real release process and a second runtime to support.
There is src/ for the source, test/ for the tests, dev/ for development tasks, bb/ and bb.edn for Babashka tasks, resources/, bin/, a .clj-kondo/ directory for the linter configuration and .lsp/ for editor tooling. The build files are deps.edn, build.clj and shadow-cljs.edn, and version.edn carries the version, which is how a Clojure project that is not Leiningen or Boot keeps the version in one place.
There are also the process documents: CONTRIBUTING.md, RELEASING.md, SECURITY.md and a CHANGELOG.md. A dedicated RELEASING.md in a small library is a sign of a project that cuts releases deliberately rather than occasionally.
The Babashka badge is the most interesting technical detail. Babashka is a native binary distribution of Clojure that runs programs quickly, and a library that is Babashka compatible can be used in scripts and in build tooling without a JVM startup cost. The badge therefore tells you that this library is usable outside a test run, which pairs with the standalone namespace and with the bb/ tasks in the repository.
The primary language is Clojure, the licence is a custom one that GitHub cannot classify with the terms in the LICENSE file, and the repository topic is nubank-stable, which is a topic for packages that Nubank treats as stable within its own dependency management. There are no GitHub releases, so the version lives in version.edn and the Clojars badge, and the last push to the repository was on 2026-09-25.
Where matcher-combinators is the wrong tool
There are cases where this library is the wrong choice, and one of them is a performance trap the README warns about directly.
The trap is the factorial matchers. Matching sets is an O(n!) operation because it compares every expected element with every actual element in order to find a best match for each one, removing matched elements from both sets as it goes. The same applies to in-any-order on long sequences. The README says to avoid applying these to large sets and long sequences.
That is fine for the typical assertion, where the set has three elements and the sequence has four. It is a problem the moment somebody uses in-any-order on a thousand-item collection in a test that runs on every commit, and the failure will look like a slow test rather than a misused matcher.
The second limit is that the library is for Clojure. There is no equivalent for another language here, and if your codebase is not Clojure this is not a candidate. The README is also explicit that support for Midje is deprecated, so a team that standardised on Midje is working with a namespace that ships for compatibility rather than one that is being developed.
The alternative approach is to compare whole values with the equality operator in the test framework, or to pull the values apart in the test and assert on the pieces. The difference in approach is that the direct comparison asserts everything at once and fails with a value diff rather than a structural one, while hand written assertions on the pieces are verbose and break when the data shape changes.
The library is worth it when the assertions are about shape rather than value, which is the common case in Clojure where functions return maps. It is not worth it for a single scalar comparison, where the equals matcher and the overhead of a matcher object are the same thing.
The other boundary is the one the README draws for itself: the library is for testing, and the standalone namespace exists for the cases where you need the same matchers in application code, but it is not a validation framework for untrusted input.
Editorial conclusion
matcher-combinators suits a Clojure codebase whose tests compare whole maps and fail with unhelpful output, and it suits teams that want assertions to state intent rather than reproduce the expected value. It does not suit a codebase that does not use Clojure, and it does not suit assertions over large sets or long sequences, because the README is explicit that those operations are factorial in the number of elements. Check three things before you adopt it: whether your assertions mostly care about a subset of a map, since embeds semantics ignore the keys you did not name, which Clojure versions you need, since the stated compatibility is 1.8 and up and the library also carries a Babashka compatible badge, and whether you use Midje, whose support is deprecated and whose dependency is no longer included. The last push to the repository was on 2026-09-25 and the project publishes no GitHub releases.
Frequently asked questions
What is matcher-combinators?
It is a Clojure library for making assertions about nested data structures, providing composable matcher combinators as building blocks for testing functions that return nested data. It supports Clojure 1.8 and up, ships on Clojars as nubank/matcher-combinators and carries a Babashka compatible badge.
How does matcher-combinators treat a map used as an expected value?
A map is interpreted as an embeds matcher, which ignores unspecified keys, so an expected map asserts only about the keys it names. The default interpretation for everything else apart from regex is equals, and regex values use the regex matcher.
How do I use matcher-combinators with clojure.test?
Require the matcher-combinators.test namespace, which extends clojure.test's is macro with match? and thrown-match?. match? takes the expected matcher first and the expression second, and thrown-match? takes a throwable subclass, a matcher applied to the ex-data, and the expression.
Is Midje still supported by matcher-combinators?
Support is deprecated. The matcher-combinators.midje namespace continues to ship to avoid breaking changes, but midje is no longer included as a transitive dependency, so a project using it has to declare the dependency itself.
What are the performance limits of matcher-combinators?
Matching sets and the in-any-order sequence matcher are O(n!) operations, because they compare every expected element with every actual element to find a best match, removing matched elements as they go. The README advises against applying them to large sets and long sequences.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/nubank-matcher-combinators)Was this useful?