Hello, I want to report a possible regression in 6.2.0

We have some rest controllers that use jackson with @JsonTypeInfo annotations.

With 6.1.x the attached code works and returns the expected JSON: [ { "type" : "one" }, { "type" : "two" } ] With 6.2.0, the type properties are missing and [ {}, {} ] is returned.

I've tracked down, that this commit may have changed the behaviour: https://github.com/spring-projects/spring-framework/commit/e788aeb25baba51880b6c9cccbeb51fe5a6d401b (#24963)

I'm unsure, if this is "works as designed" or if that code path got lost through this commit: The method declaration public <T extends BaseType> List<T> get() may be a bit strange, but valid java syntax. When I use this method declaration public List<BaseType> get(), the controller will work again. (@quaff, @jhoeller FYI)

@RestController
public class TestController {
    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
    @JsonSubTypes({
            @JsonSubTypes.Type(value = SubType1.class, name = "one"),
            @JsonSubTypes.Type(value = SubType2.class, name = "two")
    })
    public static class BaseType {

    }

    public static class SubType1 extends BaseType {
    }

    public static class SubType2 extends BaseType {
    }

    @GetMapping(value = "/test", produces = MediaType.APPLICATION_JSON_VALUE)
    public <T extends BaseType> List<T> get() {
        List<T> list = new ArrayList<>();
        list.add((T) new SubType1());
        list.add((T) new SubType2());
        return list;
    }
}

Comment From: ThanksForAllTheFish

I think this is similar to something I observed too, but tracked to https://github.com/spring-projects/spring-framework/issues/20727

In our case, a bean defined as fun <V> kafkaProducerFactory(valueSerializer: Serializer<V>): ProducerFactory<String?, V> (org.springframework.kafka.core.ProducerFactory) was not recognized as a valid candidate to https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfiguration.java#L92 (expecting a ProducerFactory<Object, Object> kafkaProducerFactory

    @Bean
    @ConditionalOnMissingBean(KafkaTemplate.class)
    public KafkaTemplate<?, ?> kafkaTemplate(ProducerFactory<Object, Object> kafkaProducerFactory,
            ProducerListener<Object, Object> kafkaProducerListener,
            ObjectProvider<RecordMessageConverter> messageConverter) {
        PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
        KafkaTemplate<Object, Object> kafkaTemplate = new KafkaTemplate<>(kafkaProducerFactory);
        messageConverter.ifUnique(kafkaTemplate::setMessageConverter);
        map.from(kafkaProducerListener).to(kafkaTemplate::setProducerListener);
        map.from(this.properties.getTemplate().getDefaultTopic()).to(kafkaTemplate::setDefaultTopic);
        map.from(this.properties.getTemplate().getTransactionIdPrefix()).to(kafkaTemplate::setTransactionIdPrefix);
        map.from(this.properties.getTemplate().isObservationEnabled()).to(kafkaTemplate::setObservationEnabled);
        return kafkaTemplate;
    }

I changed our bean definition to return ProducerFactory<out Any, Any>, but I am also worried there might other impacts and I just have to trust our test to cover those

Comment From: jhoeller

@ThanksForAllTheFish see https://github.com/spring-projects/spring-framework/issues/33982#issuecomment-2520879941 for a tweak that might fix your scenario. Feel free to give the upcoming 6.2.1 snapshot a try...

@rPraml if your case is affected by #24963, it might require a separate fix. You could try a 6.2.1 snapshot with #33982 for a start. If the problem remains, a reproducer along the lines of a ResolvableType unit test would be great.

Comment From: jhoeller

@ThanksForAllTheFish @rPraml any chance you could give the latest 6.2.1 snapshot (to be found at repo.spring.io) a try before the 6.2.1 release on Thursday?

Comment From: rPraml

@jhoeller Thanks for the reminder. Unfortunately the problem is not yet fixed. I've written a small test case:

package org.springframework.test.web.servlet.samples.standalone;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;

/**
 * Demonstrates if returning a List&gt;? extends SomeType&lt> can be correctly serialized.
 *
 * @author Roland Praml
 */
public class GenericReturnTest {
    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
    @JsonSubTypes({
            @JsonSubTypes.Type(value = SubType1.class, name = "one"),
            @JsonSubTypes.Type(value = SubType2.class, name = "two")
    })
    public static class BaseType {

    }

    public static class SubType1 extends BaseType {
    }

    public static class SubType2 extends BaseType {
    }

    @Test
    public void genericReturnTest() throws Exception {

        standaloneSetup(new GenericReturnTest.Controller()).build()
                .perform(get("/genericReturnList").accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(content().contentType("application/json"))
                .andExpect(jsonPath("$[0].type").value("one"))
                .andExpect(jsonPath("$[1].type").value("two"));
    }

    @RestController
    public static class Controller {
        @GetMapping(value = "/genericReturnList", produces = MediaType.APPLICATION_JSON_VALUE)
        public <T extends BaseType> List<T> get() {
            List<T> list = new ArrayList<>();
            list.add((T) new SubType1());
            list.add((T) new SubType2());
            return list;
        }

        @GetMapping(value = "/genericReturnCs", produces = MediaType.APPLICATION_JSON_VALUE)
        public <T extends String> List<T> get2() {
            List<T> list = new ArrayList<>();
            list.add((T) "a");
            list.add((T) "b");
            return list;
        }

    }
}

I've tracked down the difference to ResolvableType.forClassWithGenerics(Class<?> clazz, ResolvableType... generics) and I found this commit https://github.com/spring-projects/spring-framework/commit/e788aeb25baba51880b6c9cccbeb51fe5a6d401b with bisect.

With the failing commit I get as input: grafik

Before the commit, I get as input: grafik

You see, that in the failing case, the type.toString() is ? in the other case it is the BaseType

I assume, that there is a resolve missing. Changing the method as below will fix my problem (but breaks other tests)

        Type[] arguments = new Type[generics.length];
        for (int i = 0; i < generics.length; i++) {
            ResolvableType generic = generics[i];
-           Type argument = (generic != null ? generic.getType() : null);
+           Type argument = (generic != null ? generic.resolve() : null);
            arguments[i] = (argument != null && !(argument instanceof TypeVariable) ? argument : variables[i]);
        }   

Unfortunately, I don't know enough about what's going on here in detail, so it's hard for me to provide a good solution, but I hope my research can help you resolve the issue.

Comment From: jhoeller

@rPraml thanks for the research and the test case! Let's sort this out for 6.2.2 then.

Comment From: rPraml

This didn't leave me alone and I made a second attempt. Maybe you can take a look at the provided PR.

(It's OK, if this comes with 6.2.2 or later, as we have a workaround for now)

Comment From: snicoll

Closing in favor of the PR #34086