How do you assert a JSON serialization round-trip with JacksonTester and JsonContent?
answer
- write -> JsonContent; parseObject -> object
- extractingJsonPathStringValue / NumberValue
- isEqualToJson (lenient) vs isStrictlyEqualToJson
- doesNotHaveJsonPath for hidden fields
- Round-trip = both directions vs same fixture
basics
~20 sSerialize 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 sUnder @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@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
Know write + assert JSON path, and parseObject to go back to an object.
Use isEqualToJson with a fixture file and JSON-path value asserts; run both directions.
Distinguish lenient vs strict compare, verify field omission for security, and rely on the app's real Jackson config for fidelity.
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.