I'm doing some early migration tests of an application based on Spring Boot 3.5.0 and Spring Cloud 2025.0.0 to Spring Boot 4.0.0-M1 and Spring Cloud 2025.1.0-M1.
I encountered an issue with spring-cloud-stream-binder-kafka 5.0.0-M1 using Jackson 3.0.0-rc6 and Spring Boot 4.0.0-M1 using Jackson 2.19.2.
One of my REST controllers fails to marshal a JSON body to a Java object. If I remove the dependency on spring-cloud-stream-binder-kafka (and Jackson 3.0.0-rc6), the problem disappears. So I guess that the two major versions of Jackson conflict with each other.
Are the two Jackson versions expected to work side by side in a Spring Boot 4 application?
I can put together a minimal reproducer application, if it can be of help.
Comment From: wilkinsona
Please try the latest 4.0.0-SNAPSHOT. I believe this may have been fixed by the changes to HttpMessageConverters
in https://github.com/spring-projects/spring-boot/commit/067b4204b324c798b793046d55d1c7a367af57f4.
Comment From: magnus-larsson
No, unfortunately, 4.0.0-SNAPSHOT did not help.
Comment From: wilkinsona
Thanks for trying the snapshot. In that case, a minimal reproducer would certainly be helpful.
Comment From: magnus-larsson
Ok, here are instructions for reproducing the error!
Run the failing test:
git clone https://github.com/magnus-larsson/SB4.0-WebTestClient.git
cd SB4.0-WebTestClient
./gradlew test -i
The test will fail with the output:
ApplicationTests > createCompositeProductTest() STANDARD_OUT
2025-08-04T15:40:14.201+02:00 INFO 43346 --- [SB4.0-WebTestClient] [ Test worker] [ ] c.e.sb40.webtestclient.ApplicationTests : ### sending productId: 5
2025-08-04T15:40:14.248+02:00 INFO 43346 --- [SB4.0-WebTestClient] [flux-http-nio-2] [ ] c.e.sb40.webtestclient.MyRestController : ### receiving productId: 0
2025-08-04T15:40:14.252+02:00 INFO 43346 --- [SB4.0-WebTestClient] [ Test worker] [ ] c.e.sb40.webtestclient.ApplicationTests : ### test result: 0
ApplicationTests > createCompositeProductTest() FAILED
org.opentest4j.AssertionFailedError: expected: <5> but was: <0>
Edit build.gradle
and comment out the dependency to :spring-cloud-starter-stream-kafka'
, then rerun the test and it will succeed.
Comment From: wilkinsona
Thanks. I've learned from the sample that you're using the reactive stack, something that you hadn't mentioned before. HttpMessageConverters
isn't used with reactive which is why that change made no difference.
On the reactive side, Spring Framework will always use Jackson 3 when it's on the classpath even when Boot has explicitly configured it with default Jackson 2-based JSON encoder and decoder. This may be a Framework bug (/cc @sdeleuze) but it can be worked around with the following configuration:
@Bean
CodecCustomizer codecCustomizer(ObjectMapper objectMapper) {
return (codecs) -> {
codecs.registerDefaults(false);
CustomCodecs customCodecs = codecs.customCodecs();
customCodecs.register(new Jackson2JsonDecoder(objectMapper));
customCodecs.register(new Jackson2JsonEncoder(objectMapper));
};
}
The above ensures that the reactive stack uses Jackson 2 for JSON encoding and decoding. It will improve out of the box in future Spring Boot 4 milestones as the stack moves towards Jackson 3.
However, with that move in mind, the projectId
being zero when Jackson 3's being used is interesting. It can be reproduced with pure Jackson:
JsonMapper jsonMapper = tools.jackson.databind.json.JsonMapper.builder().build();
ProductAggregate read = jsonMapper.readValue("{\"productId\":5, \"name\":\"test\", \"weight\":42}", ProductAggregate.class);
System.out.println(read.getProductId());
This will output 0. If you remove the default constructor from ProductAggregate
it will output 5. Alternatively, you can keep the default constructor and annotate the other constructor with @JsonCreator
and it will also output 5. With Jackson 2, neither of these changes is necessary:
String json = "{\"productId\":5, \"name\":\"test\", \"weight\":42}";
ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();
System.out.println(objectMapper.readValue(json, ProductAggregate.class).getProductId());
The above will output 5 with:
- two constructors, no
@JsonCreator
- one constructor
- two constructors, one with
@JsonCreator
I don't know if this change in behavior in Jackson 3 is intentional. I suspect that it may not be as I can't find any mention of it in the 3.0 release notes. You may want to raise a Jackson issue for this. /cc @cowtowncoder.
I'm going to close this one now in favor of https://github.com/spring-projects/spring-boot/issues/45535 as I don't think there's anything that we can do to specifically address the problem reported here. A Framework change to allow Jackson 2 to be used as the default encoder/decoder could improve the situation. In the meantime the workaround to configure custom codecs will hopefully suffice. I don't think Boot can, even temporarily, apply the workaround for everyone as it may lose other codecs that their application needs. Alternatively, you can stick with Jackson 3 and use @JsonCreator
or remove the default constructor until a possible Jackson change is made.
Comment From: cowtowncoder
There could be a bug: behavior wrt construct detection should not change intentionally, with one exception: functionality of jackson-module-parameter-names
is embedded in databind 3.0. So if that module was not registered with 2.x, that could lead to different auto-detection.
Feel free to file an issue with jackson-databind
with relevant POJOs.