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
}
}