Unit Testing JSON Serialization with @JsonTest
Overview
Provides patterns for unit testing JSON serialization and deserialization using Spring's @JsonTest and Jackson. Covers POJO mapping, custom serializers, field name mappings, nested objects, date/time formatting, and polymorphic types.
When to Use
-
Testing JSON serialization/deserialization of DTOs
-
Verifying custom Jackson serializers/deserializers
-
Validating @JsonProperty , @JsonIgnore , and field name mappings
-
Testing date/time format handling (LocalDateTime, Date)
-
Testing null handling and missing fields
-
Testing polymorphic type deserialization
Instructions
-
Annotate test class with @JsonTest → Enables JacksonTester auto-configuration
-
Autowire JacksonTester for target type → Provides type-safe JSON assertions
-
Test serialization → Call json.write(object) and assert JSON paths with extractingJsonPath*
-
Test deserialization → Call json.parse(json) or json.parseObject(json) and assert object state
-
Validate round-trip → Serialize, then deserialize, verify same data (if object is properly comparable)
-
Test edge cases → Null values, missing fields, empty collections, invalid JSON
-
Add validation checkpoints: After each assertion, verify the test fails meaningfully with wrong data
Examples
Maven Setup
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-json</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
Gradle Setup
dependencies { implementation("org.springframework.boot:spring-boot-starter-json") testImplementation("org.springframework.boot:spring-boot-starter-test") }
Basic Serialization and Deserialization
@JsonTest class UserDtoJsonTest {
@Autowired private JacksonTester<UserDto> json;
@Test void shouldSerializeUserToJson() throws Exception { UserDto user = new UserDto(1L, "Alice", "alice@example.com", 25); JsonContent<UserDto> result = json.write(user);
result
.extractingJsonPathNumberValue("$.id").isEqualTo(1)
.extractingJsonPathStringValue("$.name").isEqualTo("Alice")
.extractingJsonPathStringValue("$.email").isEqualTo("alice@example.com")
.extractingJsonPathNumberValue("$.age").isEqualTo(25);
}
@Test void shouldDeserializeJsonToUser() throws Exception { String json_content = "{"id":1,"name":"Alice","email":"alice@example.com","age":25}"; UserDto user = json.parse(json_content).getObject();
assertThat(user.getId()).isEqualTo(1L);
assertThat(user.getName()).isEqualTo("Alice");
assertThat(user.getEmail()).isEqualTo("alice@example.com");
assertThat(user.getAge()).isEqualTo(25);
}
@Test void shouldHandleNullFields() throws Exception { String json_content = "{"id":1,"name":null,"email":"alice@example.com"}"; UserDto user = json.parse(json_content).getObject(); assertThat(user.getName()).isNull(); } }
Custom JSON Properties
public class Order { @JsonProperty("order_id") private Long id;
@JsonProperty("total_amount") private BigDecimal amount;
@JsonIgnore private String internalNote; }
@JsonTest class OrderJsonTest {
@Autowired private JacksonTester<Order> json;
@Test void shouldMapJsonPropertyNames() throws Exception { String json_content = "{"order_id":123,"total_amount":99.99}"; Order order = json.parse(json_content).getObject(); assertThat(order.getId()).isEqualTo(123L); assertThat(order.getAmount()).isEqualByComparingTo(new BigDecimal("99.99")); }
@Test void shouldIgnoreJsonIgnoreFields() throws Exception { Order order = new Order(123L, new BigDecimal("99.99")); order.setInternalNote("Secret"); assertThat(json.write(order).json).doesNotContain("internalNote"); } }
Nested Objects
public class Product { private Long id; private String name; private Category category; private List<Review> reviews; }
@JsonTest class ProductJsonTest {
@Autowired private JacksonTester<Product> json;
@Test void shouldSerializeNestedObjects() throws Exception { Product product = new Product(1L, "Laptop", new Category(1L, "Electronics")); JsonContent<Product> result = json.write(product);
result
.extractingJsonPathNumberValue("$.category.id").isEqualTo(1)
.extractingJsonPathStringValue("$.category.name").isEqualTo("Electronics");
}
@Test void shouldDeserializeNestedObjects() throws Exception { String json_content = "{"id":1,"name":"Laptop","category":{"id":1,"name":"Electronics"}}"; Product product = json.parse(json_content).getObject(); assertThat(product.getCategory().getName()).isEqualTo("Electronics"); }
@Test void shouldHandleListOfNestedObjects() throws Exception { String json_content = "{"id":1,"reviews":[{"rating":5},{"rating":4}]}"; Product product = json.parse(json_content).getObject(); assertThat(product.getReviews()).hasSize(2); } }
Date/Time Formatting
@JsonTest class DateTimeJsonTest {
@Autowired private JacksonTester<Event> json;
@Test void shouldFormatDateTimeCorrectly() throws Exception { LocalDateTime dt = LocalDateTime.of(2024, 1, 15, 10, 30, 0); json.write(new Event("Conference", dt)) .extractingJsonPathStringValue("$.scheduledAt").isEqualTo("2024-01-15T10:30:00"); } }
Custom Serializers
public class CustomMoneySerializer extends JsonSerializer<BigDecimal> { @Override public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider serializers) throws IOException { gen.writeString(value == null ? null : String.format("$%.2f", value)); } }
@JsonTest class CustomSerializerTest {
@Autowired private JacksonTester<Price> json;
@Test void shouldUseCustomSerializer() throws Exception { json.write(new Price(new BigDecimal("99.99"))) .extractingJsonPathStringValue("$.amount").isEqualTo("$99.99"); } }
Polymorphic Deserialization
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = CreditCard.class, name = "credit_card"), @JsonSubTypes.Type(value = PayPal.class, name = "paypal") }) public abstract class PaymentMethod { }
@JsonTest class PolymorphicJsonTest {
@Autowired private JacksonTester<PaymentMethod> json;
@Test void shouldDeserializeCreditCard() throws Exception { String json_content = "{"type":"credit_card","id":"card123"}"; assertThat(json.parse(json_content).getObject()).isInstanceOf(CreditCard.class); }
@Test void shouldDeserializePayPal() throws Exception { String json_content = "{"type":"paypal","id":"pp123"}"; assertThat(json.parse(json_content).getObject()).isInstanceOf(PayPal.class); } }
Best Practices
-
Test serialization AND deserialization for complete coverage
-
Verify JSON paths individually rather than comparing full JSON strings
-
Test null handling explicitly — null fields may be included or excluded depending on @JsonInclude
-
Use extractingJsonPath* methods for precise field assertions
-
Test round-trip: serialize an object, deserialize the JSON, verify the result matches
-
Validate edge cases: empty strings, empty collections, deeply nested structures
-
Group related assertions in a single test for clarity
Constraints and Warnings
-
@JsonTest loads limited context: Only JSON-related beans; use @SpringBootTest for full Spring context
-
Jackson version: Ensure annotation versions match the Jackson version in use
-
Date formats: ISO-8601 is default; use @JsonFormat for custom patterns
-
Null handling: Use @JsonInclude(Include.NON_NULL) to exclude nulls from serialization
-
Circular references: Use @JsonManagedReference /@JsonBackReference to prevent infinite loops
-
Immutable objects: Use @JsonCreator
- @JsonProperty for constructor-based deserialization
- Polymorphic types: @JsonTypeInfo must correctly identify the subtype for deserialization to work
Debugging Workflow
When a JSON test fails, follow this workflow:
Failure Symptom Common Cause How to Verify
JsonPath assertion fails Field name mismatch Check @JsonProperty spelling matches JSON key
Null expected but got value @JsonInclude(NON_NULL) configured Verify annotation on field/class
Deserialization returns wrong type Missing @JsonTypeInfo
Add type info property to JSON or configure subtype mapping
Date format mismatch Format string incorrect Confirm @JsonFormat(pattern=...) matches expected string
Missing field in output @JsonIgnore or transient modifier Check field for @JsonIgnore or transient keyword
Nested object is null Inner JSON missing or malformed Log parsed JSON; verify inner structure matches POJO
JsonParseException
Malformed JSON string Validate JSON syntax; check for unescaped characters
Validation checkpoint after fixing: Re-run the test — if it passes, write a complementary test for the opposite case (e.g., if you fixed null handling, add a test for non-null values to prevent regression).
References
-
Spring @JsonTest Documentation
-
Jackson ObjectMapper
-
Jackson Annotations