JsonUnit: JSON comparison that understands what you meant to assert
Compare JSON in your Unit Tests
At a glance
- What is it?
- JsonUnit is a Java library for comparing JSON in tests, with integrations for AssertJ, Hamcrest, Spring MVC, Spring WebTestClient, Spring REST clients and Kotest, and it understands type placeholders, JsonPath, ignored paths and custom matchers. It suits a Java team whose API tests fail on formatting rather than on meaning, and its lenient parsing of expected values is the one behaviour you need to understand before you trust it.
- Who is it for?
- JsonUnit suits a Java or Kotlin team testing JSON APIs that wants assertions on structure and meaning rather than on string formatting, and the AssertJ integration is the one to start with. It does not suit a project that needs a schema validator, since it compares values rather than validating against a JSON Schema, and it does not suit a team that wants an unambiguous string comparison, since the lenient parsing of expected values can quietly reinterpret what you wrote.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Java, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One comparison engine, several assertion dialects
JsonUnit is a library that simplifies JSON comparison in tests, and the design is one engine with several front ends. The README lists the APIs and says plainly that they all have more or less the same features, with the usage slightly different. The AssertJ integration is the recommended API because it combines the power of JsonUnit with AssertJ. Then there are Hamcrest matchers, Spring MVC assertions, Spring WebTestClient, Spring RestTestClient, Spring REST client assertions, and Kotest assertions. The point of having both an AssertJ and a Hamcrest surface is not duplication for its own sake. A team that has standardised on Hamcrest matchers and a team that has standardised on AssertJ fluent assertions should both be able to use the same comparison semantics without one of them converting to the other's style, and the underlying difference reporting is the same either way.
The repository is a multi-module Maven build and the module names map onto those APIs directly. json-unit-core holds the comparison engine, json-unit-assertj is the AssertJ integration, json-unit-kotest the Kotest one, json-unit-spring the Spring set, json-unit is the main artifact, and json-unit-bom is the bill of materials that keeps the versions aligned across them. The tests/ directory holds the test modules, and a Maven wrapper is checked in as mvnw and mvnw.cmd so a build does not need a system Maven. The KEYS file and the release signing section of the README indicate that releases are signed, and RELEASES.md and VINTAGE.md sit at the root alongside the Maven POM. The project is published to Maven Central under the net.javacrumbs.json-unit group, and the licence is the Apache License 2.0.
The AssertJ surface is where most teams will start
The recommended API reads like a normal AssertJ assertion with JSON awareness, and the README's first example is a plain document comparison.
assertThatJson("{\"a\":1, \"b\":2}").isEqualTo("{b:2, a:1}");Note that the expected value is not strict JSON. It is missing quotes around the keys, and it still compares equal, because the expected side is parsed leniently. Objects passed in are serialized automatically before comparison, so you can compare a POJO against a JSON string. AssertJ's own map assertions are available, with numbers converted to BigDecimal so that integer and floating point representations do not cause a spurious failure. Then there are the node level assertions, which is where most of the value is. You can point at a path and assert the type of what is there.
assertThatJson("{\"a\": \"value\"")
.node("a").isString().isEqualTo("value");Arrays can be asserted with contains, and the array order can be ignored when that is what you mean.
assertThatJson("{\"a\":[{\"b\": 1}, {\"c\": 1}, {\"d\": 1}]}")
.when(Option.IGNORING_ARRAY_ORDER).node("a").isArray()
.isEqualTo(json("[{\"c\": 1}, {\"b\": 1} ,{\"d\": 1}]"));The node and inPath methods both navigate, with inPath taking a JsonPath expression and node taking a dotted path. There is also a composite form, where an and block applies several assertions to different parts of the same document, and the AssertJ conditions can be combined so that a field is allowed to be either absent or null.
Lenient parsing of the expected value is the trap
This is the section of the README that anyone adopting JsonUnit should read twice. JsonUnit tries to be clever when parsing the expected value. If the value can be parsed as valid JSON, it is parsed so, and if it cannot, it is treated as a plain string to compare. The README says it usually works, but that it can lead to unexpected situations, usually with primitive values like numbers and booleans. The example is a string field holding a digit, where the expected value gets parsed as the number one and then fails to match the string.
assertThatJson("{\"id\":\"1\", \"children\":[{\"parentId\":\"1\"}]}")
.inPath("children[*].parentId")
.isArray()
.containsOnly("1");The fix is to wrap the expected value so it is not parsed.
assertThatJson("{\"id\":\"1\", \"children\":[{\"parentId\":\"1\"}]}")
.inPath("children[*].parentId")
.isArray()
.containsOnly(value("1"));The same applies to booleans, since the string true is valid JSON and gets parsed to the primitive. And the reverse case has its own helper, since if you want the expected value to be treated as JSON rather than as a string, you use JsonAssertions.json(). So there are two distinct wrappers with two distinct meanings, and picking the wrong one produces a test that fails for a reason that has nothing to do with your code. That is the whole risk of a lenient parser, and it is the reason to read the README rather than infer the behaviour from the examples.
Placeholders, ignored paths and custom matchers
The feature set is what makes JsonUnit more than a string comparison. Type placeholders let you assert that a value is of a type without pinning its exact content, using the json-unit.any-number style marker.
assertThatJson("{\"a\":1, \"b\": {\"c\" :3}}")
.isObject().containsValue(json("{\"c\" :\"${json-unit.any-number}\"}"));The custom matcher feature is the more powerful half, because you can register a named matcher and then reference it from the expected value.
assertThatJson("{\"test\":-1}")
.withConfiguration(c -> c.withMatcher("positive", greaterThan(valueOf(0))))
.isEqualTo("{\"test\": \"${json-unit.matches:positive}\"}");That turns an assertion about a value into a predicate, so you can assert that a number is positive, that a string matches a pattern, or that a timestamp falls in a range, without writing the whole document by hand. Alongside that, the README lists options for ignoring values, ignoring elements, ignoring paths and using regular expressions, so a test can say that one volatile field does not matter while everything else must match exactly. There is also a DifferenceListener feature, which is the part that decides how a failure reads. A JSON comparison library whose failure message is a wall of text is barely better than a string comparison, and a listener lets you control how the difference is rendered.
The other documented behaviours are worth knowing because they surprise people. Numbers are compared numerically rather than textually, so 1 and 1.0 are equal, with the AssertJ integration converting to BigDecimal for that reason. Array indexing is supported for navigating into arrays. Dots in keys can be escaped, which matters when your data uses dotted keys. And the expected value parsing can be made lenient explicitly, so a team can turn the surprising behaviour off rather than working around it.
Spring and Kotest surfaces, and a Kotlin API
The Spring coverage is the part that decides whether JsonUnit earns its place in an existing service. The README lists separate integrations for Spring MVC assertions, Spring WebTestClient, Spring RestTestClient and the Spring REST client assertions, which means the comparison semantics can be used whether your test calls a controller directly, goes through the reactive test client, or uses the blocking REST client. That matters because each of those produces a response body in a slightly different place, and writing the same comparison three times is how test suites drift apart.
There is also a Kotest integration for the Kotlin side of the house, and the README shows a Kotlin-flavoured API with a different import, using a trailing lambda with the assertions inside it.
import net.javacrumbs.jsonunit.assertj.assertThatJson
assertThatJson("""{"root":{"a":1, "b": 2}}""") {
isObject
node("root.a").isEqualTo(1)
node("root.b").isEqualTo(2)
}A separate section covers moving between vanilla AssertJ and JsonUnit with asInstanceOf, so a record with a JSON string field can be checked with a plain AssertJ assertion on one property and a JSON assertion on the other. The Jackson object mapper customisation section is the other one to look at. JsonUnit has to parse the documents it compares, and the way it constructs its mapper affects how numbers, dates and unknown fields are handled, so a team with strict date or number handling on its DTOs will want to know that knob exists. The repository is Maven based with a checked in wrapper, and the release process is documented with release signing.
Where JsonUnit is the wrong tool
Three cases stand out. The first is schema validation rather than comparison. JsonUnit compares one document against another, and if what you need is to assert that a payload satisfies a JSON Schema, a schema validation library is the tool for that job and it will give you a much better error message about which constraint failed. JsonUnit tells you that two things differ, not that a thing is invalid. The second is the risk of the lenient expected value parsing combined with how much you rely on it. The behaviour is convenient when you write an expected value that looks like JSON and annoying when you write a string that happens to be valid JSON. In a large suite, the failure mode is a test that fails for a parsing reason and gets rewritten until it passes, which trains people to stop reading the failure output. The third is a project that does not want a test scoped dependency with a large surface. The library pulls in AssertJ or Hamcrest, and for Spring users the Spring modules as well, and the module count means you are choosing a small set of artifacts that need to stay version aligned, which is what the json-unit-bom is for.
There is a further practical limitation in the framework support itself. JsonUnit runs inside a JVM test, so it says nothing about what your JSON actually looks like on the wire in production, and it does not tell you whether your API is fast, only whether the payload matched. On the maintenance side, the last push was on 2026-10-15, so the tree is being worked on, but the repository publishes no GitHub releases, so the version history lives in the Maven Central artifacts and the RELEASES.md file rather than in tags, and a team that wants to pin a version should pin the Maven coordinate rather than rely on a repository tag.
Editorial conclusion
JsonUnit suits a Java or Kotlin team testing JSON APIs that wants assertions on structure and meaning rather than on string formatting, and the AssertJ integration is the one to start with. It does not suit a project that needs a schema validator, since it compares values rather than validating against a JSON Schema, and it does not suit a team that wants an unambiguous string comparison, since the lenient parsing of expected values can quietly reinterpret what you wrote. Before you adopt it, read the lenient parsing section of the README and decide whether to wrap ambiguous expected values in value(), check the comparison options you need, and note that the last push was on 2026-10-15.
Frequently asked questions
How do I add JsonUnit to a Maven project?
Add the module for the assertion library you use, for example json-unit-assertj with the groupId net.javacrumbs.json-unit, the artifactId json-unit-assertj, a version and a test scope. The repository is a multi-module Maven build with a checked in mvnw wrapper.
Why does my JsonUnit comparison fail when the expected value is a string like "1"?
JsonUnit parses the expected value as JSON when it can, so the string 1 is read as the number 1 and does not match a string field. Wrap the expected value with JsonAssertions.value() to prevent parsing, or use JsonAssertions.json() when you deliberately want the value parsed as JSON.
Which assertion libraries does JsonUnit integrate with?
The README lists AssertJ, Hamcrest matchers, Spring MVC assertions, Spring WebTestClient, Spring RestTestClient, Spring REST client assertions and Kotest. The AssertJ integration is described as the recommended API.
Can JsonUnit assert that a value is positive rather than a specific number?
Yes. You register a custom matcher with withConfiguration and withMatcher, then reference it from the expected value with the json-unit.matches placeholder, so the assertion becomes a predicate rather than an exact value.
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/lukas-krecan-jsonunit)Was this useful?