Skip to content

Preserve generic SQS listener payload types - #1666

Open
brun0-4ugusto wants to merge 1 commit into
awspring:mainfrom
brun0-4ugusto:fix/sqs-generic-payload-deserialization
Open

Preserve generic SQS listener payload types#1666
brun0-4ugusto wants to merge 1 commit into
awspring:mainfrom
brun0-4ugusto:fix/sqs-generic-payload-deserialization

Conversation

@brun0-4ugusto

@brun0-4ugusto brun0-4ugusto commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📢 Type of change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring

📜 Description

This PR fixes generic payload deserialization for @SqsListener methods.

Previously, payload type inference retained only the raw Class<?>. Types such as GenericWrapper<TestEvent> and Message<List<TestEvent>> were therefore reduced to GenericWrapper.class and List.class. Without the complete generic type, Jackson deserialized nested values as LinkedHashMap instances.

This change:

  • Introduces MethodPayloadMetadata, containing the raw payload class and an optional conversion hint.
  • Preserves the listener's MethodParameter as the SmartMessageConverter conversion hint.
  • Propagates the payload class and conversion hint through the endpoint, listener container, message source, and MessageConversionContext.
  • Uses SmartMessageConverter#fromMessage(Message, Class, Object) when a conversion hint is available.
  • Falls back to the regular MessageConverter contract for converters that do not implement SmartMessageConverter.
  • Preserves custom payload type mapper precedence over inferred listener metadata.
  • Supports nested generic payloads and batch elements, including:
    • GenericWrapper<TestEvent>
    • GenericWrapper<List<TestEvent>>
    • Message<List<TestEvent>>
    • List<GenericWrapper<TestEvent>>
    • List<Message<GenericWrapper<TestEvent>>>
  • Keeps existing MethodPayloadTypeInferrer implementations source-compatible through a default metadata adaptation method.
  • Updates the reference documentation and the MessageSource Javadoc to describe where payload conversion occurs.

💡 Motivation and Context

@SqsListener payload deserialization currently loses generic type information because the inferred listener type is transported only as a raw Class<?>.

As a result, Jackson cannot determine the concrete type of generic fields or collection elements and falls back to LinkedHashMap.

The problem is also visible before listener invocation because SQS payload conversion happens at the MessageSource level. Therefore, interceptors, error handlers, and acknowledgement callbacks may also receive incorrectly typed generic values.

Spring's SmartMessageConverter already supports a conversion hint. Its base converters can use a MethodParameter hint to recover the complete generic Type, so this PR propagates that existing Spring metadata instead of introducing custom Jackson-specific type resolution.

Fixes #1597

💚 How did you test it?

Added and executed focused tests covering:

  • Payload metadata inference for simple, wrapper, message, collection, and batch listener parameters.
  • Backwards compatibility for custom MethodPayloadTypeInferrer implementations.
  • Propagation through endpoint, container, message source, and conversion context.
  • SmartMessageConverter hint invocation and regular MessageConverter fallback.
  • Custom payload type mapper precedence.
  • Conversion hint cleanup when the payload type is reconfigured.
  • Generic and nested generic deserialization with both Jackson 3 and the legacy Jackson 2 converter.
  • LocalStack integration scenarios for wrappers, nested collections, message wrappers, batches, interceptors, and acknowledgement callbacks.

Validation results:

  • Unit and converter tests passed.
  • All SqsPayloadTypeInferenceIntegrationTests passed with LocalStack.
  • Spotless validation for all changed Java files passed.
  • The complete SQS module suite executed 658 tests.

Commands used:
./mvnw -pl spring-cloud-aws-sqs -am test
Full module: Tests run: 658, Failures: 0, Errors: 0, Skipped: 5

📝 Checklist

  • I reviewed submitted code
  • I added tests to verify changes
  • I updated reference documentation to reflect the change
  • All tests passing
  • No breaking changes

🔮 Next steps

Subject to maintainer feedback, a separate follow-up PR could expand generic payload inference for listener methods inherited from generic superclasses.

For example:

abstract class GenericListener<T> {

    @SqsListener("events")
    void listen(GenericWrapper<T> event) {
    }
}

class TestEventListener extends GenericListener<TestEvent> {
}


When Spring discovers the listener method, its original declaration still describes the payload as GenericWrapper. To deserialize it as GenericWrapper, the MethodParameter must also be resolved against the concrete listener class (TestEventListener). This allows Spring's type resolution infrastructure to substitute T with TestEvent before passing the conversion hint to the SmartMessageConverter.
I have already explored and implemented this extension in a separate branch, including tests for inherited generic listener methods and nested or batch generic payload shapes. It is deliberately not included in this PR so that the initial fix remains focused and easier to review.
We can open a discussion about the expected scope and compatibility requirements for inherited generic listeners. If the maintainers agree with the direction, the existing implementation can be refined and submitted as a separate follow-up PR.

@github-actions github-actions Bot added component: sqs SQS integration related issue type: documentation Documentation or Samples related issue labels Aug 6, 2026
@brun0-4ugusto

Copy link
Copy Markdown
Contributor Author

Hi, everyone! How are you?

Are there any updates on this PR? Please let me know if you need any additional information from me.

I’m looking forward to this feature because I’d like to use it in a project at my company. 😊

Thank you, and have a lovely day!

@tomazfernandes

@tomazfernandes tomazfernandes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brun0-4ugusto thanks for the PR, looking good.

Left a few comments around backwards compatibility for existing overrides and keeping the delegation between the old and new overloads one-directional.

* target type. Note that type mappers in MessagingMessageConverters take precedence over this type.
* @param payloadDeserializationType the target class
*/
public void setPayloadDeserializationType(@Nullable Class<?> payloadDeserializationType) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's have this method delegate to the new overload with a null conversion hint.

That way both setters converge on the 3-arg hook, whose default already delegates to this 2-arg hook, so delegation flows in one direction and existing 2-arg overrides remain invoked from every entry point.

protected void doConfigurePayloadTypeOnContext(Class<?> payloadType, MessageConversionContext context) {
ConfigUtils.INSTANCE.acceptIfInstance(context, SqsMessageConversionContext.class,
ctx -> ctx.setPayloadClass(payloadType));
doConfigurePayloadTypeOnContext(payloadType, null, context);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For backwards compatibility, let's invert the delegation here: keep the payload class assignment in the 2-arg override, and have the 3-arg override call it before setting the hint.

@@ -190,6 +193,21 @@ public void setPhase(int phase) {
*/
public void setPayloadDeserializationType(@Nullable Class<?> payloadDeserializationType) {
this.payloadDeserializationType = payloadDeserializationType;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's delegate to setPayloadDeserializationType with a null conversion hint, and have the 2-arg overload assign both fields directly instead of calling this method.

That makes the richer overload the single write site, so both fields are updated together.

@@ -40,4 +40,20 @@ public interface MethodPayloadTypeInferrer {
@Nullable

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's deprecate this method, routing implementations to the new method instead.

@brun0-4ugusto

Copy link
Copy Markdown
Contributor Author

Thank you for the feedback @tomazfernandes ! I really appreciated the points you raised in the review. I’m already working on the requested changes, and I’ll update the PR with the fixes later this week.

Pass listener MethodParameter metadata through the container and message source as a SmartMessageConverter conversion hint so Jackson can resolve complete generic payload types. Add unit, converter, batch, and LocalStack integration coverage for wrappers and nested collections.

Fixes awspringgh-1597
@brun0-4ugusto
brun0-4ugusto force-pushed the fix/sqs-generic-payload-deserialization branch from 6e8a166 to 22eb49d Compare August 25, 2026 01:17
@brun0-4ugusto

Copy link
Copy Markdown
Contributor Author

Hi @tomazfernandes ! Thanks again for the review. I’ve addressed the comments and updated the PR accordingly.

Regarding MethodPayloadTypeInferrer, I deprecated inferPayloadType(...) and routed the framework implementation through the new inferPayloadMetadata(...) method.

I intentionally kept inferPayloadType(...) as the SAM of the @FunctionalInterface to preserve backward compatibility with existing custom implementations and lambdas. Making inferPayloadMetadata(...) the new abstract method would change the functional interface contract and could break existing implementations.

Could you please take another look when you have a chance? In particular, I’d appreciate your feedback on whether preserving the existing SAM is the direction you had in mind, or whether you would prefer changing it despite the compatibility impact.

Thank you! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component: sqs SQS integration related issue type: documentation Documentation or Samples related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SQS Listener deserializes POJOs with generics types incorrectly

2 participants