Affects: 5.3.1 (Spring boot 2.4.0)


Let's say I have abstract Shape class with Square and Circle subclasses annotated as Jackson "polymorphic" type (see complete example bellow). When serializing those classes to JSON Jackson adds "discriminant" property kind so it is possible to determine which subclass was serialized.

When I have REST controller with GET method returning Shape class serialization works correctly and Jackson adds kind property. Also when returning List<Shape> serialized JSON is correct. But when returning custom generic wrapper class Result<T> containing some Shape then JSON doesn't contain kind property.

Here is complete Java example code:

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import java.util.List;
import java.util.Optional;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
public class SpringTestApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringTestApplication.class, args);
    }

    @RestController
    public static class TestController {

        // works correctly
        @RequestMapping("/test")
        public Shape test() {
            return new Circle();
        }

        // works correctly
        @RequestMapping("/testList")
        public List<Shape> testList() {
            return List.of(new Circle());
        }

        // here is the issue
        @RequestMapping("/testResult")
        public Result<Shape> testResult() {
            return new Result<>(new Circle());
        }

        // also Optional doesn't work correctly
        @RequestMapping("/testOptional")
        public Optional<Shape> testOptional() {
            return Optional.of(new Circle());
        }

    }

    public static class Result<T> {
        public T result;

        public Result(T result) {
            this.result = result;
        }
    }

    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind")
    @JsonSubTypes({
        @JsonSubTypes.Type(Square.class),
        @JsonSubTypes.Type(Circle.class),
    })
    public abstract static class Shape {
    }

    @JsonTypeName("square")
    public static class Square extends Shape {
        public double size;
    }

    @JsonTypeName("circle")
    public static class Circle extends Shape {
        public double radius;
    }

}

Running this code demonstrated that returning Result<Shape> produces inconsistent result as summarized in following table:

URL result expected
http://localhost:8080/test {"kind":"circle","radius":0.0} ✔️
http://localhost:8080/testList [{"kind":"circle","radius":0.0}] ✔️
http://localhost:8080/testResult {"result":{"radius":0.0}} {"result":{"kind":"circle","radius":0.0}}
http://localhost:8080/testOptional {"radius":0.0} {"kind":"circle","radius":0.0}

Note: wrapping return type in ResponseEntity doesn't have any effect.

Comment From: vojtechhabarta

Another case where the JSON doesn't contain discriminant property is Optional<T>. I added this case to sample code and the table.

Comment From: miere

Hi there. Did we had any feedback on this?

I'm having a similar problem. I can confirm that the result expectation table above correctly describes the issues I'm facing at the moment.

Weirdly, though, when manually instantiating Jackson's ObjectMapper, registering all of modules available in the classpath, and serializing the object using writeValueAsString it does include the discriminant field.

Do you reckon this issue could be related to the standard ObjectMapper configuration provided by the Spring Boot application?

Comment From: bclozel

Sorry it took us so long to reply. I've tried this sample application with recent versions of Spring Boot and get the first two expected results. The optional variant also works out of the box:

http http://localhost:8080/testOptional
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/json
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
    "kind": "circle",
    "radius": 0.0
}

As for the parameterized type Result<T> I believe this is the expected behavior for Jackson due to type erasure and that you need to tell jackson to serialize the type information there:

public class Result<T> {

    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind")
    public T result;

    public Result(T result) {
        this.result = result;
    }
}

Doing so succeeds with the third case:

http http://localhost:8080/testResult
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/json
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
    "result": {
        "kind": "circle",
        "radius": 0.0
    }
}