Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5a4516f
Introduce Jackson present
MatejNedic Nov 23, 2025
13441c2
Merge branch 'main' into Introduce-jackson-3-support
MatejNedic Nov 25, 2025
a0456f4
Initial jackson 2 and 3 support
MatejNedic Nov 27, 2025
f37c006
Initial set of implementation
MatejNedic Dec 10, 2025
f4806e1
Initial set of implementation
MatejNedic Dec 11, 2025
a0c6cd2
Fix default
MatejNedic Dec 11, 2025
9e9d831
Refactor auto configuration and tests
MatejNedic Dec 21, 2025
85c486e
Update integrations and docs partially
MatejNedic Dec 22, 2025
e3420ab
Update integrations and docs partially
MatejNedic Dec 22, 2025
159d735
update
MatejNedic Dec 23, 2025
7df4d3f
Rework
MatejNedic Jan 3, 2026
79b6f3c
Refactor
MatejNedic Jan 6, 2026
4d873cb
Refactor
MatejNedic Jan 6, 2026
36894ed
refactor all tests and add sample of SQSTemplate. Idea only.
MatejNedic Jan 7, 2026
bbd6482
refactor
MatejNedic Jan 7, 2026
626cf2b
Update per comments
MatejNedic Jan 11, 2026
b668fc9
Update SqsDocs
MatejNedic Jan 11, 2026
2215ee6
Spotless and version upgrade
MatejNedic Jan 11, 2026
64da7f0
Update wrong creation of default
MatejNedic Jan 12, 2026
29be43d
Merge branch 'main' into Introduce-jackson-3-support
MatejNedic Jan 12, 2026
a9ed0b5
Fix typo
maciejwalkowiak Jan 13, 2026
a002d60
Formatting
maciejwalkowiak Jan 13, 2026
befd46d
Add missing nullable
maciejwalkowiak Jan 13, 2026
f171481
Merge remote-tracking branch 'upstream/main' into matej-jackson3
tomazfernandes Jan 14, 2026
29b56a9
Merge pull request #2 from tomazfernandes/matej-jackson3
MatejNedic Jan 14, 2026
7b86f51
clean up
MatejNedic Jan 14, 2026
22da0ff
spotless apply
MatejNedic Jan 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/src/main/asciidoc/s3.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,8 @@ s3Template.store(BUCKET, "person.json", p);
Person loadedPerson = s3Template.read(BUCKET, "person.json", Person.class);
----

By default, if Jackson is on the classpath, `S3Template` uses `ObjectMapper` based `Jackson2JsonS3ObjectConverter` to convert from S3 object to Java object and vice versa.
By default, if Jackson 3 is on the classpath, `S3Template` uses `JsonMapper` based `Jackson2JsonS3ObjectConverter` to convert from S3 object to Java object and vice versa.
If Jackson 3 is not on classpath and Jackson 2 is, `S3Template` uses `ObjectMapper` based `LegacyJackson2JsonS3ObjectConverter` to convert from S3 object to Java object and vice versa.
This behavior can be overwritten by providing custom bean of type `S3ObjectConverter`.

=== Determining S3 Objects Content Type
Expand Down
7 changes: 5 additions & 2 deletions docs/src/main/asciidoc/sns.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ The starter automatically configures and registers a `SnsTemplate` bean providin
It supports sending notifications with payload of type:

* `String` - using `org.springframework.messaging.converter.StringMessageConverter`
* `Object` - which gets serialized to JSON using `org.springframework.messaging.converter.MappingJackson2MessageConverter` and Jackson's `com.fasterxml.jackson.databind.ObjectMapper` autoconfigured by Spring Boot.
* `Object` - which if Jackson 3 is on classpath gets serialized to JSON using `org.springframework.messaging.converter.JacksonJsonMessageConverter` and Jackson's `tools.jackson.databind.json.JsonMapper` autoconfigured by Spring Boot.
* `Object` - which if Jackson 3 is not on classpath but Jackson 2 is gets serialized to JSON using `org.springframework.messaging.converter.MappingJackson2MessageConverter` and Jackson's `com.fasterxml.jackson.databind.ObjectMapper` autoconfigured by Spring Boot.

Additionally, it exposes handful of methods supporting `org.springframework.messaging.Message`.

Expand Down Expand Up @@ -305,7 +306,9 @@ The `SnsInboundChannelAdapter` is an extension of `HttpRequestHandlingMessagingG
Its URL must be used from the AWS Management Console to add this endpoint as a subscriber to the SNS Topic.
However, before receiving any notification itself, this HTTP endpoint must confirm the subscription.

See `SnsInboundChannelAdapter` JavaDocs for more information.
If you want to use Jackson 3 with spring cloud aws integrations you should use `SnsInboundChannelAdapter`. If you are still using Jackson 2 you should use `LegacyJackson2SnsInboundChannelAdapter`.

See `SnsInboundChannelAdapter` and `LegacyJackson2SnsInboundChannelAdapter` JavaDocs for more information.

An important option of this adapter to consider is `handleNotificationStatus`.
This `boolean` flag indicates if the adapter should send `SubscriptionConfirmation/UnsubscribeConfirmation` message to the `output-channel` or not.
Expand Down
44 changes: 32 additions & 12 deletions docs/src/main/asciidoc/sqs.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,10 @@ For convenience, the `additionalInformation` parameters can be found as constant
Message conversion by default is handled by a `SqsMessagingMessageConverter` instance, which contains:

* `SqsHeaderMapper` for mapping headers to and from `messageAttributes`
* `CompositeMessageConverter` with a `StringMessageConverter` and a `MappingJackson2MessageConverter` for converting payloads to and from JSON.
* `CompositeMessageConverter` with a `StringMessageConverter` and a `JacksonJsonMessageConverter` for converting payloads to and from JSON.

NOTE: `SqsMessagingMessageConverter` is using Jackson 3 under the hood. If you are interested in using Jackson 2 you should be configuring `LegacyJackson2SqsMessagingMessageConverter`.
When `LegacyJackson2SqsMessagingMessageConverter` is used you have to configure `MappingJackson2MessageConverter`.

A custom `MessagingMessageConverter` implementation can be provided in the `SqsTemplate.builder()`:

Expand All @@ -344,14 +347,26 @@ SqsOperations template = SqsTemplate
.builder()
.sqsAsyncClient(sqsAsyncClient)
.configureDefaultConverter(converter -> {
converter.setObjectMapper(objectMapper);
converter.setHeaderMapper(headerMapper);
converter.setPayloadTypeHeader("my-custom-type-header");
}
)
.buildSyncTemplate();
```

The Jackson 2 specific `LegacyJackson2SqsMessagingMessageConverter` instance can also be configured in the builder:

```java
SqsOperations template = SqsTemplate
.builder()
.sqsAsyncClient(sqsAsyncClient)
.configureDefaultConverter(converter -> {
converter.setHeaderMapper(headerMapper);
converter.setPayloadTypeHeader("my-custom-type-header");
}, SqsJacksonVersion.JACKSON_2)
.buildSyncTemplate();
```

===== Specifying a Payload Class for Receive Operations

By default, the `SqsTemplate` adds a header with name `JavaType` containing the fully qualified name of the payload class to all messages sent.
Expand Down Expand Up @@ -1615,11 +1630,14 @@ public SqsMessageListenerContainerFactory<Object> defaultSqsListenerContainerFac
----
=== Message Conversion and Payload Deserialization

Payloads are automatically deserialized from `JSON` for `@SqsListener` annotated methods using a `MappingJackson2MessageConverter`.
Payloads are automatically deserialized from `JSON` for `@SqsListener` annotated methods using a `JacksonJsonMessageConverter` when Jackson 3 is on classpath. If there is no Jackson 3 on classpath and there is Jackson 2 on classpath `MappingJackson2MessageConverter` will be used.

NOTE: When using Spring Boot's auto-configuration, if there's a single `ObjectMapper` in Spring Context, such object mapper will be used for converting messages.
NOTE: When using Spring Boot's auto-configuration, if there's a single `JsonMapper` in Spring Context, such object mapper will be used for converting messages.
This includes the one provided by Spring Boot's auto-configuration itself.
For configuring a different `JsonMapper`, see <<Global Configuration for @SqsListeners>>.

NOTE: When Jackson 3 is not on classpath and only Jackson 2 is found, if there's a single `ObjectMapper` in Spring Context, such object mapper will be used for converting messages.
This includes the one provided by Spring Boot's auto-configuration itself.
For configuring a different `ObjectMapper`, see <<Global Configuration for @SqsListeners>>.

==== Automatic Payload Type Inference

Expand Down Expand Up @@ -1693,7 +1711,9 @@ It is also possible not to include payload type information in the header by usi
More complex mapping can be achieved by using the `setPayloadTypeMapper` method, which overrides the default header-based mapping.
This method receives a `Function<Message<?>, Class<?>> payloadTypeMapper` that will be applied to incoming messages.

The default `MappingJackson2MessageConverter` can be replaced by using the `setPayloadMessageConverter` method.
The default `JacksonJsonMessageConverter` can be replaced by using the `setPayloadMessageConverter` method.

`MappingJackson2MessageConverter` can also be replaced by using the `setPayloadMessageConverter` method.

The framework also provides the `SqsHeaderMapper`, which implements the `HeaderMapper` interface and is invoked by the `SqsMessagingMessageConverter`.
To provide a different `HeaderMapper` implementation, use the `setHeaderMapper` method.
Expand All @@ -1720,7 +1740,7 @@ headerMapper.setAdditionalHeadersFunction(((sqsMessage, accessor) -> {
messageConverter.setHeaderMapper(headerMapper);

// Configure Payload Converter
MappingJackson2MessageConverter payloadConverter = new MappingJackson2MessageConverter();
JacksonJsonMessageConverter payloadConverter = new JacksonJsonMessageConverter();
payloadConverter.setPrettyPrint(true);
messageConverter.setPayloadMessageConverter(payloadConverter);

Expand Down Expand Up @@ -2007,25 +2027,25 @@ The following attributes can be configured in the registrar:
- `setMessageHandlerMethodFactory` - provide a different factory to be used to create the `invocableHandlerMethod` instances that wrap the listener methods.
- `setListenerContainerRegistry` - provide a different `MessageListenerContainerRegistry` implementation to be used to register the `MessageListenerContainers`
- `setMessageListenerContainerRegistryBeanName` - provide a different bean name to be used to retrieve the `MessageListenerContainerRegistry`
- `setObjectMapper` - set the `ObjectMapper` instance that will be used to deserialize payloads in listener methods.
- `setJacksonMessageConverterMigration` - set the `JacksonMessageConverterMigration` which will be used to construct `MessageConverter` and provide either `JsonMapper` for Jackson 3 or `ObjectMapper` for Jackson 2. Check `JacksonJsonMessageConverterMigration` and `LegacyJackson2MessageConverterMigration`
See <<Message Conversion and Payload Deserialization>> for more information on where this is used.
- `setValidator` - set the `Validator` instance that will be used for payload validation in listener methods.
- `manageMessageConverters` - gives access to the list of message converters that will be used to convert messages.
By default, `StringMessageConverter`, `SimpleMessageConverter` and `MappingJackson2MessageConverter` are used.
By default, `StringMessageConverter`, `SimpleMessageConverter` and `JacksonJsonMessageConverter` are used.

- `manageArgumentResolvers` - gives access to the list of argument resolvers that will be used to resolve the listener method arguments.
The order of resolvers is important - `PayloadMethodArgumentResolver` should generally be last since it's used as default.
- `setMethodPayloadTypeInferrer` - set the `MethodPayloadTypeInferrer` instance to be used for automatic payload type inference.
Set to `null` to disable automatic inference and rely on header-based type mapping.
See <<Automatic Payload Type Inference>> for more information.

A simple example would be:
A simple example for Jackson 3 would be:

[source, java]
----
@Bean
SqsListenerConfigurer configurer(ObjectMapper objectMapper) {
return registrar -> registrar.setObjectMapper(objectMapper);
SqsListenerConfigurer configurer(JacksonJsonMessageConverterFactory factory) {
return registrar -> registrar.setJacksonMessageConverterFactory(factory);
}
----

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-build</artifactId>
<version>5.0.0-RC1</version>
<version>5.0.0</version>
<relativePath/><!-- lookup parent from repository -->
</parent>

Expand Down
5 changes: 5 additions & 0 deletions spring-cloud-aws-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@
<artifactId>spring-cloud-aws-ses</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-sns</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

import io.awspring.cloud.autoconfigure.AwsSyncClientCustomizer;
import io.awspring.cloud.autoconfigure.config.AbstractAwsConfigDataLocationResolver;
import io.awspring.cloud.autoconfigure.core.*;
import io.awspring.cloud.autoconfigure.core.AwsProperties;
import io.awspring.cloud.autoconfigure.core.CredentialsProperties;
import io.awspring.cloud.autoconfigure.core.RegionProperties;
import io.awspring.cloud.autoconfigure.s3.S3ClientCustomizer;
import io.awspring.cloud.autoconfigure.s3.properties.S3Properties;
import java.util.ArrayList;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
/*
* Copyright 2013-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.awspring.cloud.autoconfigure.config.secretsmanager;

import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;

public class SecretsManagerExceptionHappenedAnalyzer extends AbstractFailureAnalyzer<AwsSecretsManagerPropertySourceNotFoundException> {
public class SecretsManagerExceptionHappenedAnalyzer
extends AbstractFailureAnalyzer<AwsSecretsManagerPropertySourceNotFoundException> {

@Override
protected FailureAnalysis analyze(Throwable rootFailure, AwsSecretsManagerPropertySourceNotFoundException cause) {
return new FailureAnalysis("Could not import properties from AWS Secrets Manager. Exception happened while trying to load the keys: " + cause.getMessage(),
"Depending on error message determine action course", cause);
return new FailureAnalysis(
"Could not import properties from AWS Secrets Manager. Exception happened while trying to load the keys: "
+ cause.getMessage(),
"Depending on error message determine action course", cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,15 @@
import io.awspring.cloud.autoconfigure.core.AwsConnectionDetails;
import io.awspring.cloud.autoconfigure.core.AwsProperties;
import io.awspring.cloud.autoconfigure.s3.properties.S3Properties;
import io.awspring.cloud.s3.InMemoryBufferingS3OutputStreamProvider;
import io.awspring.cloud.s3.Jackson2JsonS3ObjectConverter;
import io.awspring.cloud.s3.PropertiesS3ObjectContentTypeResolver;
import io.awspring.cloud.s3.S3ObjectContentTypeResolver;
import io.awspring.cloud.s3.S3ObjectConverter;
import io.awspring.cloud.s3.S3Operations;
import io.awspring.cloud.s3.S3OutputStreamProvider;
import io.awspring.cloud.s3.S3ProtocolResolver;
import io.awspring.cloud.s3.S3Template;
import io.awspring.cloud.s3.*;
import java.util.Optional;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.*;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.context.annotation.Bean;
Expand All @@ -49,6 +45,7 @@
import software.amazon.awssdk.services.s3.S3ClientBuilder;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.encryption.s3.S3EncryptionClient;
import tools.jackson.databind.json.JsonMapper;

/**
* {@link AutoConfiguration} for {@link S3Client} and {@link S3ProtocolResolver}.
Expand Down Expand Up @@ -124,11 +121,50 @@ else if (awsProperties.getEndpoint() != null) {
return builder.build();
}

@Bean
@ConditionalOnMissingBean
S3Client s3Client(S3ClientBuilder s3ClientBuilder) {
return s3ClientBuilder.build();
}

@Bean
@ConditionalOnMissingBean
S3OutputStreamProvider inMemoryBufferingS3StreamProvider(S3Client s3Client,
Optional<S3ObjectContentTypeResolver> contentTypeResolver) {
return new InMemoryBufferingS3OutputStreamProvider(s3Client,
contentTypeResolver.orElseGet(PropertiesS3ObjectContentTypeResolver::new));
}

@Conditional(S3EncryptionConditional.class)
@ConditionalOnClass(name = "software.amazon.encryption.s3.S3EncryptionClient")
@Configuration
public static class S3EncryptionConfiguration {

private static void configureEncryptionProperties(S3Properties properties,
ObjectProvider<S3RsaProvider> rsaProvider, ObjectProvider<S3AesProvider> aesProvider,
S3EncryptionClient.Builder builder) {
PropertyMapper propertyMapper = PropertyMapper.get();
var encryptionProperties = properties.getEncryption();

propertyMapper.from(encryptionProperties::isEnableDelayedAuthenticationMode)
.to(builder::enableDelayedAuthenticationMode);
propertyMapper.from(encryptionProperties::isEnableLegacyUnauthenticatedModes)
.to(builder::enableLegacyUnauthenticatedModes);
propertyMapper.from(encryptionProperties::isEnableMultipartPutObject).to(builder::enableMultipartPutObject);

if (!StringUtils.hasText(properties.getEncryption().getKeyId())) {
if (aesProvider.getIfAvailable() != null) {
builder.aesKey(aesProvider.getObject().generateSecretKey());
}
else {
builder.rsaKeyPair(rsaProvider.getObject().generateKeyPair());
}
}
else {
propertyMapper.from(encryptionProperties::getKeyId).to(builder::kmsKeyId);
}
}

@Bean
@ConditionalOnMissingBean
S3Client s3EncryptionClient(S3EncryptionClient.Builder s3EncryptionBuilder, S3ClientBuilder s3ClientBuilder) {
Expand All @@ -154,55 +190,28 @@ S3EncryptionClient.Builder s3EncrpytionClientBuilder(S3Properties properties,
configureEncryptionProperties(properties, rsaProvider, aesProvider, builder);
return builder;
}
}

private static void configureEncryptionProperties(S3Properties properties,
ObjectProvider<S3RsaProvider> rsaProvider, ObjectProvider<S3AesProvider> aesProvider,
S3EncryptionClient.Builder builder) {
PropertyMapper propertyMapper = PropertyMapper.get();
var encryptionProperties = properties.getEncryption();

propertyMapper.from(encryptionProperties::isEnableDelayedAuthenticationMode)
.to(builder::enableDelayedAuthenticationMode);
propertyMapper.from(encryptionProperties::isEnableLegacyUnauthenticatedModes)
.to(builder::enableLegacyUnauthenticatedModes);
propertyMapper.from(encryptionProperties::isEnableMultipartPutObject).to(builder::enableMultipartPutObject);
@Configuration
@AutoConfigureAfter(Jackson2JsonS3ObjectConverterConfiguration.class)
@ConditionalOnClass(name = "com.fasterxml.jackson.databind.ObjectMapper")
static class LegacyJackson2JsonS3ObjectConverterConfiguration {

if (!StringUtils.hasText(properties.getEncryption().getKeyId())) {
if (aesProvider.getIfAvailable() != null) {
builder.aesKey(aesProvider.getObject().generateSecretKey());
}
else {
builder.rsaKeyPair(rsaProvider.getObject().generateKeyPair());
}
}
else {
propertyMapper.from(encryptionProperties::getKeyId).to(builder::kmsKeyId);
}
@ConditionalOnMissingBean
@Bean
S3ObjectConverter s3ObjectConverter(Optional<ObjectMapper> objectMapper) {
return new LegacyJackson2JsonS3ObjectConverter(objectMapper.orElseGet(ObjectMapper::new));
}
}

@Bean
@ConditionalOnMissingBean
S3Client s3Client(S3ClientBuilder s3ClientBuilder) {
return s3ClientBuilder.build();
}

@Configuration
@ConditionalOnClass(ObjectMapper.class)
@ConditionalOnClass(name = "tools.jackson.databind.json.JsonMapper")
static class Jackson2JsonS3ObjectConverterConfiguration {

@ConditionalOnMissingBean
@Bean
S3ObjectConverter s3ObjectConverter(Optional<ObjectMapper> objectMapper) {
return new Jackson2JsonS3ObjectConverter(objectMapper.orElseGet(ObjectMapper::new));
S3ObjectConverter s3ObjectConverter(Optional<JsonMapper> jsonMapper) {
return new Jackson2JsonS3ObjectConverter(jsonMapper.orElseGet(JsonMapper::new));
}
}

@Bean
@ConditionalOnMissingBean
S3OutputStreamProvider inMemoryBufferingS3StreamProvider(S3Client s3Client,
Optional<S3ObjectContentTypeResolver> contentTypeResolver) {
return new InMemoryBufferingS3OutputStreamProvider(s3Client,
contentTypeResolver.orElseGet(PropertiesS3ObjectContentTypeResolver::new));
}
}
Loading