Description
I have a generic class which wraps another generic class which itself wraps a couple of other classes. I want the JSON from all these to be "flattened", so I use the @JsonUnwrapped
annotation. I can then GET and POST the flattened JSON and it all works fine. But if I then introduce some abstract (generic) REST controller, so that the method which receives the POSTed data is in a generic class, Spring fails to deserialize the JSON correctly.
For clarity, here's some code that explains the situation...
First, I have some DTOs representing information about a user:
public class UserBasicInfo {
private String name;
// ... getters and setters
}
public class UserExtendedInfo {
private String roleName;
// ... getters and setters
}
I then have a generic class that I use to combine this "basic" and "extended" information:
public class ItemInfo<Basic, Extended> {
@JsonUnwrapped
private Basic basicInfo;
@JsonUnwrapped
private Extended extendedInfo;
// ... getters and setters
}
I then have a generic class which wraps the generic class above and adds more information to it (e.g an ID):
public class SavedItem<Id, Info> {
private Id id;
@JsonUnwrapped
private Info info;
// ... getters and setters
}
In the end, what I work with is an object of this type: SavedItem<Integer, ItemInfo<UserBasicInfo, UserExtendedInfo>>
If I then have a controller which receives an item of that type, as shown below, it all works OK:
@RestController
public class UserRestController {
@PostMapping
public void handle(@RequestBody SavedItem<Integer, ItemInfo<UserBasicInfo, UserExtendedInfo>> item) {
}
}
However, if I declare that method instead in an abstract generic base class, as shown below, it doesn't quite work:
public abstract class AbstractRestController<Id, Basic, Extended> {
public void handle(SavedItem<Id, ItemInfo<Basic, Extended>> item) {
// Here, item.getInfo().getBasicInfo() returns null, as does getExtendedInfo()
}
}
@RestController
public class UserRestController extends AbstractRestController<Integer, UserBasicInfo, UserExtendedInfo> {
}
What happens is that the basicInfo
and extendedInfo
items are left null as the JSON fails to be deserialized correctly.
The issue does not occur if:
- The concrete class,
UserRestController
, explicitly implements the method(even if it just overrides it and then calls the super class's implementation),
Or
- There is only one generic class rather than two, e.g. if the method receives
ItemInfo<Basic, Extended>
instead ofSavedItem<Id, ItemInfo<Basic, Extended>>
Or
- I don't use
@JsonUnwrapped
(meaning the JSON has a couple of extra levels of nesting within it).
The reason I think this might be an issue with Spring rather than Jackson is because if I put a breakpoint in AbstractJackson2HttpMessageConverter.readJavaType()
(line 239) I can see that when the Jackson object mapper is called, the javaType
argument which is passed to it doesn't have any of the generic information in the ItemInfo
class. Whereas when that same line of code is hit when the REST controller method is implemented in the concrete class, then the full generic type information is correctly passed into Jackson. However that's just a guess, so if you think this is actually a Jackson issue let me know and I'll log an issue with them.
A git repo where the issue can be replicated is available here. Details of how to replicate it are provided in the readme in that repo.
The version information is:
- Spring Boot 2.1.4.RELEASE
- Spring Framework 5.1.6.RELEASE
- Jackson 2.9.8