From 215aee344004c79a4c7cb124e227bbb389093a58 Mon Sep 17 00:00:00 2001 From: JiHongKim98 Date: Mon, 23 Mar 2026 19:04:07 +0900 Subject: [PATCH] Prevent default message round-trip failures for module-backed payloads The default mapper bootstrap only disabled unknown-property failures, so payloads that depend on Jackson module discovery could serialize on enqueue and still fail during dequeue or listener argument mapping. This change enables module auto-discovery in SerializationUtils and adds a regression test that verifies the default GenericMessageConverter round-trip path with a ServiceLoader-discovered Jackson module. Constraint: Must preserve the existing MessageConverterProvider-based extension model Rejected: Prefer Spring-managed ObjectMapper by default | changes the library's default configuration contract Confidence: medium Scope-risk: narrow Directive: If default mapper bootstrap changes again, keep equivalent regression coverage for service-loaded modules Tested: ./gradlew checkFormatJava :rqueue-core:test --tests com.github.sonus21.rqueue.converter.GenericMessageConverterTest --tests com.github.sonus21.rqueue.converter.JsonMessageConverterTest Not-tested: Cross-service producer/consumer environments with different classpaths --- .../rqueue/utils/SerializationUtils.java | 1 + .../GenericMessageConverterTest.java | 90 +++++++++++++++++++ .../tools.jackson.databind.JacksonModule | 1 + 3 files changed, 92 insertions(+) create mode 100644 rqueue-core/src/test/resources/META-INF/services/tools.jackson.databind.JacksonModule diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/SerializationUtils.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/SerializationUtils.java index 30ed3fb1..74da045d 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/SerializationUtils.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/SerializationUtils.java @@ -39,6 +39,7 @@ public static boolean isJson(String data) { public static ObjectMapper createObjectMapper() { return JsonMapper.builder() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .findAndAddModules() .build(); } } diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/converter/GenericMessageConverterTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/converter/GenericMessageConverterTest.java index 1db2fc7d..b6c411f0 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/converter/GenericMessageConverterTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/converter/GenericMessageConverterTest.java @@ -42,6 +42,15 @@ import org.junit.jupiter.api.Test; import org.springframework.messaging.Message; import org.springframework.messaging.support.GenericMessage; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.deser.std.StdDeserializer; +import tools.jackson.databind.module.SimpleModule; +import tools.jackson.databind.ser.std.StdSerializer; @SuppressWarnings("unchecked") @CoreUnitTest @@ -115,6 +124,16 @@ void toAndFromMessageList() { assertEquals(dataList, fromMessage); } + @Test + void toAndFromMessageUsingServiceLoadedModules() { + ServiceLoadedPayload payload = ServiceLoadedPayload.of("module-backed"); + Message message = (Message) + genericMessageConverter.toMessage(payload, RqueueMessageHeaders.emptyMessageHeaders()); + ServiceLoadedPayload fromMessage = + (ServiceLoadedPayload) genericMessageConverter.fromMessage(message, null); + assertEquals(payload, fromMessage); + } + @Test void envelopeEventToAndFromMessage() { Event event = new Event<>("evt-1", comment); @@ -456,4 +475,75 @@ public Notification(String id, String message, int priority) { this.priority = priority; } } + + public static class ServiceLoadedPayload { + + private final String value; + + private ServiceLoadedPayload(String value) { + this.value = value; + } + + public static ServiceLoadedPayload of(String value) { + return new ServiceLoadedPayload(value); + } + + String value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ServiceLoadedPayload)) { + return false; + } + ServiceLoadedPayload that = (ServiceLoadedPayload) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + } + + public static class ServiceLoadedPayloadModule extends SimpleModule { + + public ServiceLoadedPayloadModule() { + addSerializer(ServiceLoadedPayload.class, new ServiceLoadedPayloadSerializer()); + addDeserializer(ServiceLoadedPayload.class, new ServiceLoadedPayloadDeserializer()); + } + + private static class ServiceLoadedPayloadSerializer + extends StdSerializer { + + private ServiceLoadedPayloadSerializer() { + super(ServiceLoadedPayload.class); + } + + @Override + public void serialize( + ServiceLoadedPayload value, JsonGenerator jsonGenerator, SerializationContext provider) + throws JacksonException { + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringProperty("value", value.value()); + jsonGenerator.writeEndObject(); + } + } + + private static class ServiceLoadedPayloadDeserializer + extends StdDeserializer { + + private ServiceLoadedPayloadDeserializer() { + super(ServiceLoadedPayload.class); + } + + @Override + public ServiceLoadedPayload deserialize(JsonParser jsonParser, DeserializationContext context) + throws JacksonException { + JsonNode node = jsonParser.readValueAsTree(); + return ServiceLoadedPayload.of(node.get("value").asText()); + } + } + } } diff --git a/rqueue-core/src/test/resources/META-INF/services/tools.jackson.databind.JacksonModule b/rqueue-core/src/test/resources/META-INF/services/tools.jackson.databind.JacksonModule new file mode 100644 index 00000000..c0743c06 --- /dev/null +++ b/rqueue-core/src/test/resources/META-INF/services/tools.jackson.databind.JacksonModule @@ -0,0 +1 @@ +com.github.sonus21.rqueue.converter.GenericMessageConverterTest$ServiceLoadedPayloadModule