home / skills / giuseppe-trisciuoglio / developer-kit / unit-test-json-serialization

This skill provides patterns for unit testing JSON serialization and deserialization with Jackson and JsonTest to validate mappings and formats.

npx playbooks add skill giuseppe-trisciuoglio/developer-kit --skill unit-test-json-serialization

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
12.2 KB
---
name: unit-test-json-serialization
description: Provides patterns for unit testing JSON serialization/deserialization with Jackson and @JsonTest. Use when validating JSON mapping, custom serializers, and date format handling.
category: testing
tags: [junit-5, json-test, jackson, serialization, deserialization]
version: 1.0.1
allowed-tools: Read, Write, Bash, Glob, Grep
---

# Unit Testing JSON Serialization with @JsonTest

## Overview

This skill provides patterns for unit testing JSON serialization and deserialization using Spring's @JsonTest and Jackson. It covers testing POJO mapping, custom serializers/deserializers, field name mappings, nested objects, date/time formatting, polymorphic types, and null handling without full Spring context.

## When to Use

Use this skill when:
- Testing JSON serialization of DTOs
- Testing JSON deserialization to objects
- Testing custom Jackson serializers/deserializers
- Verifying JSON field names and formats
- Testing null handling in JSON
- Want fast JSON mapping tests without full Spring context

## Instructions

1. **Use @JsonTest annotation**: Configure test context with @JsonTest for JacksonTester auto-configuration
2. **Test both serialization and deserialization**: Verify objects serialize to JSON and JSON deserializes to objects
3. **Use JacksonTester**: Autowire JacksonTester for type-safe JSON assertions
4. **Test null handling**: Verify null fields are handled correctly (included or excluded)
5. **Test nested objects**: Verify complex nested structures serialize/deserialize properly
6. **Test date/time formats**: Verify LocalDateTime, Date, and other temporal types format correctly
7. **Test custom serializers**: Verify @JsonSerialize and custom JsonSerializer implementations work
8. **Use JsonPath assertions**: Extract and verify specific JSON paths with JsonPath matchers

## Examples

## Setup: JSON Testing

### Maven
```xml
<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>
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
</dependency>
```

### Gradle
```kotlin
dependencies {
  implementation("org.springframework.boot:spring-boot-starter-json")
  implementation("com.fasterxml.jackson.core:jackson-databind")
  testImplementation("org.springframework.boot:spring-boot-starter-test")
}
```

## Basic Pattern: @JsonTest

### Test JSON Serialization

```java
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;

@JsonTest
class UserDtoJsonTest {

  @Autowired
  private JacksonTester<UserDto> json;

  @Test
  void shouldSerializeUserToJson() throws Exception {
    UserDto user = new UserDto(1L, "Alice", "[email protected]", 25);

    org.assertj.core.data.Offset result = json.write(user);

    result
      .extractingJsonPathNumberValue("$.id").isEqualTo(1)
      .extractingJsonPathStringValue("$.name").isEqualTo("Alice")
      .extractingJsonPathStringValue("$.email").isEqualTo("[email protected]")
      .extractingJsonPathNumberValue("$.age").isEqualTo(25);
  }

  @Test
  void shouldDeserializeJsonToUser() throws Exception {
    String json_content = "{\"id\":1,\"name\":\"Alice\",\"email\":\"[email protected]\",\"age\":25}";

    UserDto user = json.parse(json_content).getObject();

    assertThat(user)
      .isNotNull()
      .hasFieldOrPropertyWithValue("id", 1L)
      .hasFieldOrPropertyWithValue("name", "Alice")
      .hasFieldOrPropertyWithValue("email", "[email protected]")
      .hasFieldOrPropertyWithValue("age", 25);
  }

  @Test
  void shouldHandleNullFields() throws Exception {
    String json_content = "{\"id\":1,\"name\":null,\"email\":\"[email protected]\",\"age\":null}";

    UserDto user = json.parse(json_content).getObject();

    assertThat(user.getName()).isNull();
    assertThat(user.getAge()).isNull();
  }
}
```

## Testing Custom JSON Properties

### @JsonProperty and @JsonIgnore

```java
public class Order {
  @JsonProperty("order_id")
  private Long id;

  @JsonProperty("total_amount")
  private BigDecimal amount;

  @JsonIgnore
  private String internalNote;

  private LocalDateTime createdAt;
}

@JsonTest
class OrderJsonTest {

  @Autowired
  private JacksonTester<Order> json;

  @Test
  void shouldMapJsonPropertyNames() throws Exception {
    String json_content = "{\"order_id\":123,\"total_amount\":99.99,\"createdAt\":\"2024-01-15T10:30:00\"}";

    Order order = json.parse(json_content).getObject();

    assertThat(order.getId()).isEqualTo(123L);
    assertThat(order.getAmount()).isEqualByComparingTo(new BigDecimal("99.99"));
  }

  @Test
  void shouldIgnoreJsonIgnoreAnnotatedFields() throws Exception {
    Order order = new Order(123L, new BigDecimal("99.99"));
    order.setInternalNote("Secret note");

    JsonContent<Order> result = json.write(order);

    assertThat(result.json).doesNotContain("internalNote");
  }
}
```

## Testing List Deserialization

### JSON Arrays

```java
@JsonTest
class UserListJsonTest {

  @Autowired
  private JacksonTester<List<UserDto>> json;

  @Test
  void shouldDeserializeUserList() throws Exception {
    String jsonArray = "[{\"id\":1,\"name\":\"Alice\"},{\"id\":2,\"name\":\"Bob\"}]";

    List<UserDto> users = json.parseObject(jsonArray);

    assertThat(users)
      .hasSize(2)
      .extracting(UserDto::getName)
      .containsExactly("Alice", "Bob");
  }

  @Test
  void shouldSerializeUserListToJson() throws Exception {
    List<UserDto> users = List.of(
      new UserDto(1L, "Alice"),
      new UserDto(2L, "Bob")
    );

    JsonContent<List<UserDto>> result = json.write(users);

    result.json.contains("Alice").contains("Bob");
  }
}
```

## Testing Nested Objects

### Complex JSON Structures

```java
public class Product {
  private Long id;
  private String name;
  private Category category;
  private List<Review> reviews;
}

public class Category {
  private Long id;
  private String name;
}

public class Review {
  private String reviewer;
  private int rating;
  private String comment;
}

@JsonTest
class ProductJsonTest {

  @Autowired
  private JacksonTester<Product> json;

  @Test
  void shouldSerializeNestedObjects() throws Exception {
    Category category = new Category(1L, "Electronics");
    Product product = new Product(1L, "Laptop", category);

    JsonContent<Product> result = json.write(product);

    result
      .extractingJsonPathNumberValue("$.id").isEqualTo(1)
      .extractingJsonPathStringValue("$.name").isEqualTo("Laptop")
      .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())
      .isNotNull()
      .hasFieldOrPropertyWithValue("name", "Electronics");
  }

  @Test
  void shouldHandleListOfNestedObjects() throws Exception {
    String json_content = "{\"id\":1,\"name\":\"Laptop\",\"reviews\":[{\"reviewer\":\"John\",\"rating\":5},{\"reviewer\":\"Jane\",\"rating\":4}]}";

    Product product = json.parse(json_content).getObject();

    assertThat(product.getReviews())
      .hasSize(2)
      .extracting(Review::getRating)
      .containsExactly(5, 4);
  }
}
```

## Testing Date/Time Formatting

### LocalDateTime and Other Temporal Types

```java
@JsonTest
class DateTimeJsonTest {

  @Autowired
  private JacksonTester<Event> json;

  @Test
  void shouldFormatDateTimeCorrectly() throws Exception {
    LocalDateTime dateTime = LocalDateTime.of(2024, 1, 15, 10, 30, 0);
    Event event = new Event("Conference", dateTime);

    JsonContent<Event> result = json.write(event);

    result.extractingJsonPathStringValue("$.scheduledAt").isEqualTo("2024-01-15T10:30:00");
  }

  @Test
  void shouldDeserializeDateTimeFromJson() throws Exception {
    String json_content = "{\"name\":\"Conference\",\"scheduledAt\":\"2024-01-15T10:30:00\"}";

    Event event = json.parse(json_content).getObject();

    assertThat(event.getScheduledAt())
      .isEqualTo(LocalDateTime.of(2024, 1, 15, 10, 30, 0));
  }
}
```

## Testing Custom Serializers

### Custom JsonSerializer Implementation

```java
public class CustomMoneySerializer extends JsonSerializer<BigDecimal> {
  @Override
  public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
    if (value == null) {
      gen.writeNull();
    } else {
      gen.writeString(String.format("$%.2f", value));
    }
  }
}

public class Price {
  @JsonSerialize(using = CustomMoneySerializer.class)
  private BigDecimal amount;
}

@JsonTest
class CustomSerializerTest {

  @Autowired
  private JacksonTester<Price> json;

  @Test
  void shouldUseCustomSerializer() throws Exception {
    Price price = new Price(new BigDecimal("99.99"));

    JsonContent<Price> result = json.write(price);

    result.extractingJsonPathStringValue("$.amount").isEqualTo("$99.99");
  }
}
```

## Testing Polymorphic Deserialization

### Type Information in JSON

```java
@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 {
  private String id;
}

@JsonTest
class PolymorphicJsonTest {

  @Autowired
  private JacksonTester<PaymentMethod> json;

  @Test
  void shouldDeserializeCreditCard() throws Exception {
    String json_content = "{\"type\":\"credit_card\",\"id\":\"card123\",\"cardNumber\":\"****1234\"}";

    PaymentMethod method = json.parse(json_content).getObject();

    assertThat(method).isInstanceOf(CreditCard.class);
  }

  @Test
  void shouldDeserializePayPal() throws Exception {
    String json_content = "{\"type\":\"paypal\",\"id\":\"pp123\",\"email\":\"[email protected]\"}";

    PaymentMethod method = json.parse(json_content).getObject();

    assertThat(method).isInstanceOf(PayPal.class);
  }
}
```

## Best Practices

- **Use @JsonTest** for focused JSON testing
- **Test both serialization and deserialization**
- **Test null handling** and missing fields
- **Test nested and complex structures**
- **Verify field name mapping** with @JsonProperty
- **Test date/time formatting** thoroughly
- **Test edge cases** (empty strings, empty collections)

## Common Pitfalls

- Not testing null values
- Not testing nested objects
- Forgetting to test field name mappings
- Not verifying JSON property presence/absence
- Not testing deserialization of invalid JSON

## Constraints and Warnings

- **@JsonTest loads limited context**: Only JSON-related beans are available; use @SpringBootTest for full context
- **Jackson version compatibility**: Ensure Jackson annotations match the Jackson version in use
- **Date format standards**: ISO-8601 is the default format; custom formats require @JsonFormat annotation
- **Null handling**: Use JsonInclude.Include.NON_NULL to exclude null fields from serialization
- **Circular references**: Be aware of circular references; use @JsonManagedReference/@JsonBackReference
- **Polymorphic type handling**: @JsonTypeInfo must be correctly configured for polymorphic deserialization
- **Immutable objects**: Use @JsonCreator and @JsonProperty for constructor-based deserialization

## Troubleshooting

**JacksonTester not available**: Ensure class is annotated with `@JsonTest`.

**Field name doesn't match**: Check @JsonProperty annotation and Jackson configuration.

**DateTime parsing fails**: Verify date format matches Jackson's expected format.

## References

- [Spring @JsonTest Documentation](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/autoconfigure/json/JsonTest.html)
- [Jackson ObjectMapper](https://fasterxml.github.io/jackson-databind/javadoc/2.15/com/fasterxml/jackson/databind/ObjectMapper.html)
- [JSON Annotations](https://fasterxml.github.io/jackson-annotations/javadoc/2.15/)

Overview

This skill provides practical patterns for unit testing JSON serialization and deserialization using Spring's @JsonTest and Jackson. It focuses on fast, focused tests for POJOs, custom serializers/deserializers, date/time formatting, polymorphic types, nested structures, and null handling without loading a full Spring context. Examples and pitfalls are included to make tests reliable and maintainable.

How this skill works

Annotate tests with @JsonTest to auto-configure JacksonTester for type-safe JSON assertions. Use JacksonTester to write objects to JSON and parse JSON back to objects, combine JsonPath extractions for targeted assertions, and verify behaviors like @JsonProperty mappings, @JsonIgnore exclusions, custom JsonSerializer output, and temporal formatting. Tests run quickly because only JSON-related beans are loaded.

When to use it

  • Validating DTO mapping for REST requests and responses
  • Verifying custom JsonSerializer/JsonDeserializer implementations
  • Ensuring date/time fields serialize to and parse from expected formats
  • Testing nested objects, collections, and polymorphic types
  • Checking null handling and presence/absence of JSON properties

Best practices

  • Annotate with @JsonTest and inject JacksonTester<T> for focused tests
  • Always test both serialization and deserialization for critical fields
  • Use JsonPath assertions to check specific paths instead of string matching
  • Cover nulls, empty collections, and missing fields explicitly
  • Test custom serializers and @JsonProperty mappings with dedicated examples

Example use cases

  • Unit test a UserDto to ensure field names and types map correctly to JSON
  • Verify a custom money serializer emits a '$99.99' string for BigDecimal
  • Deserialize an array of UserDto and assert order and values
  • Confirm LocalDateTime fields serialize as ISO-8601 and parse back
  • Validate polymorphic PaymentMethod deserialization using type metadata

FAQ

Why use @JsonTest instead of @SpringBootTest?

@JsonTest loads only JSON-related configuration and Jackson support, making tests faster and isolating JSON mapping behavior. Use @SpringBootTest only when you need the full application context.

How do I test custom serializers?

Annotate the target field with @JsonSerialize(using = YourSerializer.class), then use JacksonTester.write(...) and assert the JSON path contains the expected serialized string.