spring-projects / spring-framework

Spring Framework
https://spring.io/projects/spring-framework
Apache License 2.0
56.25k stars 37.98k forks source link

[3.2.0] Jackson Deserialization fails in Spring Controller (works fine in Spring 3.1.x) #31829

Closed jloisel closed 9 months ago

jloisel commented 9 months ago

We are facing an issue when upgrading to Spring Boot 3.2.0. We are currently using:

openjdk version "17.0.9" 2023-10-17
OpenJDK Runtime Environment (build 17.0.9+9-Ubuntu-122.04)
OpenJDK 64-Bit Server VM (build 17.0.9+9-Ubuntu-122.04, mixed mode, sharing)

Code is compiled using Maven with "-parameters" enabled:

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.10.1</version>
        <configuration>
          <release>${java.release.version}</release>
          <compilerArgs>
            <arg>-parameters</arg>
          </compilerArgs>
        </configuration>
      </plugin>

When upgrading from Spring Boot 3.1.x to 3.2.0, the following controller method stops working properly:

  @PostMapping
  @ResponseStatus(value = CREATED)
  public void index(
    @RequestBody final ValueWrapper<List<UserCounterValue>> wrapper) {
    final Map<Class<?>, List<UserCounterValue>> map = wrapper.getValue()
      .stream()
      .collect(groupingBy(Object::getClass));
....
}

The ValueWrapper bean is pretty simple:

@Value
public class ValueWrapper<T> {

  T value;

  @JsonCreator
  public ValueWrapper(
      @JsonProperty("value") final T value) {
    super();
    this.value = requireNonNull(value);
  }

}

UserCounterValue uses polymorphism with json "@type" field:

@JsonTypeInfo(use = NAME, include = PROPERTY)
public interface UserCounterValue extends UserEntity, BenchResultEntity {

}

And both concrete beans implementing this interface are declared in Jackson using mapper.registerSubTypes(...).

When running a junit which uses Retrofit to send the bean to the backend, it fails:

  private final NumberCounterValue number = NumberCounterValueTest.newInstance();
  private final TextualCounterValue text = TextualCounterValueTest.newInstance();

  @Test
  public void shouldIndex() {
    final List<UserCounterValue> list = ImmutableList.of(text, number);
    final Call<Void> index = service().index(new ValueWrapper<>(list));
    calls.execute(index);
  }
java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class com.octoperf.monitor.entity.api.UserCounterValue (java.util.LinkedHashMap is in module java.base of loader 'bootstrap'; com.octoperf.monitor.entity.api.UserCounterValue is in unnamed module of loader 'app')
    at java.base/java.util.stream.Collectors.lambda$groupingBy$53(Collectors.java:1142)
    at java.base/java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
    at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625)
    at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
    at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
    at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
    at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
    at com.octoperf.monitor.rest.server.CounterValuesController.index(CounterValuesController.java:42)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)

When debugging the issue, we can see that the UserCounterValue is not being deserialized properly:

image

When running the same test using Spring Boot 3.1.x:

image

In this case, we can see that the nested bean is deserialized properly using Jackson.

jloisel commented 9 months ago

I narrowed down the issue to the Spring Framework dependency. It works with Spring 6.0.14, but fails with Spring 6.1.1.

bclozel commented 9 months ago

I don't know if it makes any difference but the maven compiler plugin differs a bit from what we're recommending - do you still get the same issue with this configuration?

Could you share a minimal sample application that reproduces the problem? I'll transfer this issue to Spring Framework in the meantime.

jloisel commented 9 months ago

I tried with:

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.11.0</version>
        <configuration>
          <release>${java.release.version}</release>
          <parameters>true</parameters>
        </configuration>
      </plugin>

Still the same issue. But, i'll keep the new syntax over the old one :)

Unfortunately, I don't have any minimal sample application. I'm checking if I can do that in a reasonable timeframe.

jloisel commented 9 months ago

Here is a sample project which reproduces the issue:

demo.zip

Simply run the unit test, and it fails with a classcast exception like shown above.

bclozel commented 9 months ago

This might be related to one of the latest binding issues we fixed here in Spring Framework. I've upgraded your sample to Spring Boot 3.2.1-SNAPSHOT and it works fine. Can you check with your actual application?

jloisel commented 9 months ago

Glad it fixed the issue :)

I tried the snapshot maven repository but it's apparently not being able to pull spring snapshots:

  <repositories>
    <repository>
      <id>repository.spring.snapshot</id>
      <name>Spring Snapshot Repository</name>
      <url>https://repo.spring.io/snapshot</url>
    </repository>
  </repositories>

If you know how I can retrieve Spring Snapshots, let me know.

bclozel commented 9 months ago

Maybe this?

  <repositories>
    <repository>
      <id>repository.spring.snapshot</id>
      <name>Spring Snapshot Repository</name>
      <url>https://repo.spring.io/snapshot</url>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
      <releases>
        <enabled>false</enabled>
      </releases>
    </repository>
  </repositories>

Artifacts are present: https://repo.spring.io/snapshot/org/springframework/spring-core/6.1.2-SNAPSHOT/

I'm closing this issue for now, we will reopen if it turns out this is not completely fixed in SNAPSHOTs.

jloisel commented 9 months ago

Thanks!