Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Provides patterns for unit testing JSON serialization/deserialization with Jackson and `@JsonTest`. Validates JSON mapping, custom serializers, date formats, and polymorphic types. Use when testing JSON serialization, validating custom serializers, or writing JSON unit tests in Spring Boot applications.
.claude/skills/giuseppe-trisciuoglio-unit-test-json-serialization/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 46% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 152% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 108% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 130% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 114% | 0% |
@JsonTestProvides 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.
@JsonProperty, @JsonIgnore, and field name mappings@JsonTest → Enables JacksonTester auto-configurationjson.write(object) and assert JSON paths with extractingJsonPath*json.parse(json) or json.parseObject(json) and assert object statexml<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>
kotlindependencies { implementation("org.springframework.boot:spring-boot-starter-json") testImplementation("org.springframework.boot:spring-boot-starter-test") }
java@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(); } }
javapublic 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"); } }
javapublic 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); } }
java@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"); } }
javapublic 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"); } }
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 { } @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); } }
@JsonIncludeextractingJsonPath* methods for precise field assertions@JsonTest loads limited context: Only JSON-related beans; use @SpringBootTest for full Spring context@JsonFormat for custom patterns@JsonInclude(Include.NON_NULL) to exclude nulls from serialization@JsonManagedReference/@JsonBackReference to prevent infinite loops@JsonCreator + @JsonProperty for constructor-based deserialization@JsonTypeInfo must correctly identify the subtype for deserialization to workWhen 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).
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 15,899 | 12,756 | -20% | 1 | 1 | 0% | 3,800 | 5,562 | +46% | 0 | 0 | — |
case-02 | pass→pass | 8,327 | 7,852 | -6% | 1 | 1 | 0% | 1,610 | 3,739 | +132% | 0 | 0 | — |
case-03 | pass→pass | 12,657 | 6,034 | -52% | 1 | 1 | 0% | 2,318 | 3,487 | +50% | 0 | 0 | — |
case-04 | pass→pass | 12,669 | 7,024 | -45% | 1 | 1 | 0% | 2,065 | 3,797 | +84% | 0 | 0 | — |
case-05 | fail→pass | 7,431 | 10,581 | +42% | 1 | 1 | 0% | 1,445 | 3,641 | +152% | 0 | 0 | — |
case-06 | pass→pass | 7,932 | 7,298 | -8% | 1 | 1 | 0% | 1,593 | 4,118 | +159% | 0 | 0 | — |
case-07 | fail→pass | 9,714 | 5,490 | -43% | 1 | 1 | 0% | 1,675 | 3,489 | +108% | 0 | 0 | — |
case-08 | fail→pass | 9,929 | 7,053 | -29% | 1 | 1 | 0% | 1,725 | 3,976 | +130% | 0 | 0 | — |
case-09 | fail→pass | 10,638 | 9,448 | -11% | 1 | 1 | 0% | 1,997 | 4,274 | +114% | 0 | 0 | — |
case-10 | pass→pass | 9,349 | 8,830 | -6% | 1 | 1 | 0% | 1,653 | 3,535 | +114% | 0 | 0 | — |
case-11 | pass→pass | 14,048 | 15,098 | +7% | 1 | 1 | 0% | 2,372 | 5,084 | +114% | 0 | 0 | — |
case-12 | pass→pass | 9,048 | 3,976 | -56% | 1 | 1 | 0% | 1,758 | 3,212 | +83% | 0 | 0 | — |
case-13 | pass→pass | 8,395 | 4,517 | -46% | 1 | 1 | 0% | 1,502 | 3,364 | +124% | 0 | 0 | — |
case-14 | fail→pass | 6,930 | 5,504 | -21% | 1 | 1 | 0% | 1,225 | 3,581 | +192% | 0 | 0 | — |
case-15 | pass→pass | 11,797 | 8,542 | -28% | 1 | 1 | 0% | 1,992 | 4,089 | +105% | 0 | 0 | — |
case-16 | pass→pass | 7,863 | 7,609 | -3% | 1 | 1 | 0% | 1,293 | 3,872 | +199% | 0 | 0 | — |
case-17 | pass→pass | 10,474 | 8,381 | -20% | 1 | 1 | 0% | 1,803 | 4,032 | +124% | 0 | 0 | — |
case-18 | pass→pass | 11,457 | 9,877 | -14% | 1 | 1 | 0% | 1,919 | 4,196 | +119% | 0 | 0 | — |
case-19 | pass→pass | 13,059 | 11,330 | -13% | 1 | 1 | 0% | 2,642 | 4,652 | +76% | 0 | 0 | — |
case-20 | pass→pass | 4,638 | 6,366 | +37% | 1 | 1 | 0% | 886 | 3,748 | +323% | 0 | 0 | — |
case-21 | pass→pass | 27,873 | 11,632 | -58% | 1 | 1 | 0% | 3,174 | 4,962 | +56% | 0 | 0 | — |
case-22 | pass→pass | 10,191 | 2,928 | -71% | 1 | 1 | 0% | 1,526 | 2,967 | +94% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +27 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.