skip to content

How do you assert a JSON serialization round-trip with JacksonTester and JsonContent?

level: seniorimportance: should knowfreq 32%

answer

  1. write -> JsonContent; parseObject -> object
  2. extractingJsonPathStringValue / NumberValue
  3. isEqualToJson (lenient) vs isStrictlyEqualToJson
  4. doesNotHaveJsonPath for hidden fields
  5. Round-trip = both directions vs same fixture

basics

~20 s

Serialize with json.write(obj) to get a JsonContent, then assert with extractingJsonPath...Value or isEqualToJson against an expected file. For the reverse, json.parseObject(text) turns JSON back into the object so you can assert its fields. Doing both proves a round-trip.

solid answer

~40 s

Under @JsonTest you inject a typed JacksonTester<T>. Serialization: json.write(obj) returns a JsonContent<T>, which plugs into AssertJ. You assert individual fields with extractingJsonPathStringValue/NumberValue/BooleanValue/ArrayValue at JSON paths, or compare the whole document with isEqualToJson("expected.json") (lenient, order-insensitive) or isStrictlyEqualToJson for strict matching. Deserialization: json.parse(text) yields ObjectContent<T>, and json.parseObject(text) returns the T directly so you can assert getters. A full round-trip test both serializes to the expected shape and parses expected JSON back to an equal object, proving your @JsonProperty names, custom serializers, date/enum formatting, and null handling are stable. Because @JsonTest uses the application's real ObjectMapper (modules, @JsonComponent), these assertions reflect production output — making it effective contract-testing for DTOs.

code

java · 23 lines
java
@JsonTest
class EmployeeJsonRoundTripTest {

    @Autowired
    private JacksonTester<Employee> json;

    @Test
    void serialize_matchesContract_andHidesPassword() throws Exception {
        JsonContent<Employee> out = json.write(new Employee("Ada", 42, "secret"));

        assertThat(out).extractingJsonPathStringValue("$.name").isEqualTo("Ada");
        assertThat(out).extractingJsonPathNumberValue("$.age").isEqualTo(42);
        assertThat(out).doesNotHaveJsonPath("$.password"); // @JsonIgnore verified
        assertThat(out).isEqualToJson("employee.json");     // package-relative file
    }

    @Test
    void deserialize_reconstructsObject() throws Exception {
        Employee e = json.readObject("employee.json");       // from classpath
        assertThat(e.getName()).isEqualTo("Ada");
        assertThat(e.getAge()).isEqualTo(42);
    }
}

go deeper

for a junior

Know write + assert JSON path, and parseObject to go back to an object.

for a middle

Use isEqualToJson with a fixture file and JSON-path value asserts; run both directions.

for a senior

Distinguish lenient vs strict compare, verify field omission for security, and rely on the app's real Jackson config for fidelity.

for a principal

Treat these as API contract tests, standardize fixture management and strictness policy, and guard sensitive-field omission and date/enum representation across the DTO surface.

## Goal A **round-trip** test proves two directions of your JSON contract: object → JSON (serialization) matches the expected wire shape, and JSON → object (deserialization) reconstructs an equal object. Locking both down catches accidental field renames, formatting changes, or serializer regressions before they break API consumers. ## The tools (`@JsonTest`) - **`JacksonTester<T>`** — the typed helper; auto-initialized when the test runs under the slice (Spring calls `JacksonTester.initFields`). - **`JsonContent<T>`** — result of `write(...)`; AssertJ-friendly wrapper carrying the serialized JSON string. - **`ObjectContent<T>`** — result of `parse(...)`; carries the deserialized object; `parseObject(...)` returns the `T` straight. ## Serialization assertions on `JsonContent` `assertThat(json.write(obj))` supports: - **JSON-path value asserts**: `extractingJsonPathStringValue("$.name")`, `extractingJsonPathNumberValue("$.age")`, `extractingJsonPathBooleanValue(...)`, `extractingJsonPathArrayValue(...)`, `extractingJsonPathMapValue(...)`. - **Existence**: `hasJsonPathValue("$.address")`, `doesNotHaveJsonPath("$.password")` (great for asserting a secret is NOT serialized). - **Whole-document compare**: `isEqualToJson("employee.json")` — resolves the file on the classpath relative to the test's package; comparison is **lenient** (extra whitespace and field order ignored). Use **`isStrictlyEqualToJson(...)`** to require an exact match (no extra fields, strict arrays). You can also pass a JSON `String` or a `Resource`. ## Deserialization assertions ``` Employee e = json.parseObject(expectedJson); assertThat(e.getName()).isEqualTo("Ada"); ``` Or assert against the whole object via `ObjectContent`: ``` assertThat(json.parse(expectedJson)).isEqualTo(new Employee("Ada", 42)); ``` (requires a sensible `equals`). ## A complete round-trip 1. Build a domain object, `write` it, and assert the JSON matches `employee.json`. 2. `read`/`parseObject` `employee.json` and assert the resulting object equals the original. Both passing means serialization and deserialization agree on the same fixture. ## Why it is faithful `@JsonTest` loads the **application's real Jackson configuration** — registered **modules** (e.g. `JavaTimeModule` so `Instant` renders as ISO-8601 not a timestamp), **`@JsonComponent`** custom (de)serializers, and global features like `SerializationFeature.WRITE_DATES_AS_TIMESTAMPS`. So the test output equals what your controllers would emit. ## Gotchas - **Null/uninitialized tester** → NPE: only auto-initialized under the slice; otherwise call `JacksonTester.initFields(this, objectMapper)`. - **Lenient vs strict**: `isEqualToJson` won't catch an *extra* serialized field; use `isStrictlyEqualToJson` or an explicit `doesNotHaveJsonPath` when omission matters (security fields!). - **Fixture location**: `isEqualToJson("x.json")` is package-relative; a leading `/` makes it absolute on the classpath. - **Floating point / big decimals**: path number asserts can be finicky with type (Integer vs Long vs Double) — assert with the right `...NumberValue` and value type. - **Locale/timezone-dependent formatting** can make date assertions flaky; pin them via Jackson config.

  • isEqualToJson passes even though your object now serializes an extra 'ssn' field. Why, and how do you catch it?
    isEqualToJson is lenient — extra fields are ignored. Use isStrictlyEqualToJson to require an exact match, or add an explicit doesNotHaveJsonPath("$.ssn") assertion for sensitive omissions.
  • How would you verify that an Instant serializes as ISO-8601 rather than an epoch number?
    @JsonTest uses the app's ObjectMapper; if the JavaTimeModule is registered and WRITE_DATES_AS_TIMESTAMPS is false, assert extractingJsonPathStringValue on the field equals the ISO string. This also proves your global Jackson config is correct.

saying these in an interview costs you the question

  • Assuming isEqualToJson is strict and would flag extra/missing fields.
  • Confusing write (serialize) with parse/parseObject (deserialize).
  • Forgetting the tester must be initialized (NPE otherwise).
  • Thinking @JsonTest uses a default mapper, so custom serializers/modules are not exercised.

context