package com.ankurm.kafkabasics; import org.apache.kafka.common.errors.SerializationException; import org.apache.kafka.common.header.Headers; import org.apache.kafka.common.header.internals.RecordHeaders; import org.junit.jupiter.api.Test; import org.springframework.kafka.support.serializer.JacksonJsonDeserializer; import org.springframework.kafka.support.serializer.JacksonJsonSerializer; import org.springframework.kafka.support.serializer.JsonSerializer; import java.nio.charset.StandardCharsets; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * The two JSON serializer families Spring Kafka 4.1 ships, side by side. No broker needed: a * Serializer is a function from object to bytes and can be called directly, which is the * cheapest possible way to settle a serialisation question. * * @see docs/03-serialisation.md */ class SerialisationTest { private static final OrderEvent EVENT = OrderEvent.of("o-1", "c-1", "10.00"); @Test void theJackson2SerializerCannotWriteAnInstant() { // JsonSerializer is the Jackson 2 one. Its default ObjectMapper has no JSR-310 module, // and Jackson 2.21 refuses java.time types rather than guessing at a representation. // This is what you get by following any pre-Boot-4 tutorial. try (JsonSerializer serializer = new JsonSerializer<>()) { assertThatExceptionOfType(SerializationException.class) .isThrownBy(() -> serializer.serialize("orders", EVENT)) .withMessageContaining("Can't serialize data") .withStackTraceContaining("Java 8 date/time type `java.time.Instant` not supported by default"); } } @Test void theJackson3SerializerWritesItWithNoConfiguration() { try (JacksonJsonSerializer serializer = new JacksonJsonSerializer<>()) { String json = new String(serializer.serialize("orders", EVENT), StandardCharsets.UTF_8); System.out.println("JacksonJsonSerializer -> " + json); assertThat(json).contains("\"orderId\":\"o-1\"").contains("\"placedAt\":"); // BigDecimal survives as a number, not a string, and keeps its scale. assertThat(json).contains("\"amount\":10.00"); } } @Test void theDeserializerRefusesAnUntrustedClassNamedInTheTypeHeader() { Headers headers = new RecordHeaders(); byte[] bytes; try (JacksonJsonSerializer serializer = new JacksonJsonSerializer<>()) { // The three-argument overload is the one that writes __TypeId__. That header is how the // consumer learns which class to build, and it is also why trusted packages exist: // instantiating a class named by an inbound message is a deserialization gadget. bytes = serializer.serialize("orders", headers, EVENT); } assertThat(headers.lastHeader("__TypeId__")).isNotNull(); assertThat(new String(headers.lastHeader("__TypeId__").value(), StandardCharsets.UTF_8)) .isEqualTo(OrderEvent.class.getName()); try (JacksonJsonDeserializer deserializer = new JacksonJsonDeserializer<>()) { deserializer.configure(Map.of(), false); assertThatExceptionOfType(Exception.class) .isThrownBy(() -> deserializer.deserialize("orders", headers, bytes)) .withMessageContaining("not in the trusted packages"); } } @Test void aRoundTripThroughTheJackson3PairIsLossless() { byte[] bytes; try (JacksonJsonSerializer serializer = new JacksonJsonSerializer<>()) { bytes = serializer.serialize("orders", EVENT); } try (JacksonJsonDeserializer deserializer = new JacksonJsonDeserializer<>()) { deserializer.configure(Map.of(JacksonJsonDeserializer.VALUE_DEFAULT_TYPE, OrderEvent.class.getName(), JacksonJsonDeserializer.TRUSTED_PACKAGES, "com.ankurm.kafkabasics"), false); OrderEvent back = deserializer.deserialize("orders", bytes); assertThat(back).isEqualTo(EVENT); // BigDecimal scale survives the round trip. It would not if amount were a double. assertThat(back.amount().scale()).isEqualTo(2); } } }