Getting Spring/Jackson to intelligently deserialize model subclasses - java

Given a model hierarchy like so:
// WARNING: This is pseudo-code for giving an example!
public abstract class BaseVehicle {
private String make;
private String model;
// Constructors, getters & setters down here
}
public class Motorcycle extends BaseVehicle {
private int numCylinders;
// Constructors, getters & setters down here
}
public class Car extends BaseVehicle {
// ...etc.
}
And given the following payload class (that will be sent to a Spring controller):
public class Payload {
#JsonIgnore
#JsonProperty(value = "orgId")
private String orgId;
#JsonIgnore
#JsonProperty(value = "isInitialized")
private Boolean isInitialized;
#JsonIgnore
#JsonProperty(value = "vehicle")
private BaseVehicle vehicle;
// Constructors, getters & setters down here
}
I'm wondering if its possible to have the Spring controller (using Jackson for JSON serialization) configured to only expect a BaseVehicle instance in the Payload it receives, but to dynamically infer which BaseVehicle subclass was actually sent:
#RequestMapping(value='/payload', method=RequestMethod.POST)
#ResponseStatus(value = HttpStatus.OK)
#ResponseBody MyAppResponse onPayload(#RequestBody Payload payload) {
logger.info("Received a payload with a vehicle of type: " + payload.getVehicle().getClass().getName());
}
So that if I happen to send a Payload JSON that contains a Motorcycle as its vehicle field, then when that logger.info(...) statement fires, the code sees the vehicle is a Motorcycle (and ditto for any other BaseVehicle subclass)?
Is this possible, if so, how?

However I'd greatly prefer a solution that allows the JSON to remain as-is.
As I mentioned in my comment above, you could analyze the payload vehicle JSON object tree in order to make a little analysis trying to detect the payload element type.
#JsonDeserialize(using = BaseVehicleJsonDeserializer.class)
abstract class BaseVehicle {
#JsonProperty
private String make;
#JsonProperty
private String model;
}
#JsonDeserialize(as = Car.class)
final class Car
extends BaseVehicle {
}
#JsonDeserialize(as = Motorcycle.class)
final class Motorcycle
extends BaseVehicle {
#JsonProperty
private int numCylinders;
}
The trick here is the #JsonDeserialize annotation. The BaseVehicleJsonDeserializer can be implemented as follows:
final class BaseVehicleJsonDeserializer
extends JsonDeserializer<BaseVehicle> {
#Override
public BaseVehicle deserialize(final JsonParser parser, final DeserializationContext context)
throws IOException {
final TreeNode treeNode = parser.readValueAsTree();
final Class<? extends BaseVehicle> baseVehicleClass = Stream.of(treeNode)
// Check if the tree node is ObjectNode
.filter(tn -> tn instanceof ObjectNode)
// And cast
.map(tn -> (ObjectNode) tn)
// Now "bind" the object node with if the object node can be supported by the resolver
.flatMap(objectNode -> Stream.of(BaseVehicleTypeResolver.cachedBaseVehicleTypeResolvers).filter(resolver -> resolver.matches(objectNode)))
// If found, just get the detected vehicle class
.map(BaseVehicleTypeResolver::getBaseVehicleClass)
// Take the first resolver only
.findFirst()
// Or throw a JSON parsing exception
.orElseThrow(() -> new JsonParseException(parser, "Cannot parse: " + treeNode));
// Convert the JSON tree to the resolved class instance
final ObjectMapper objectMapper = (ObjectMapper) parser.getCodec();
return objectMapper.treeToValue(treeNode, baseVehicleClass);
}
// Known strategies here
private enum BaseVehicleTypeResolver {
CAR_RESOLVER {
#Override
protected Class<? extends BaseVehicle> getBaseVehicleClass() {
return Car.class;
}
#Override
protected boolean matches(final ObjectNode objectNode) {
return !objectNode.has("numCylinders");
}
},
MOTORCYCLE_RESOLVER {
#Override
protected Class<? extends BaseVehicle> getBaseVehicleClass() {
return Motorcycle.class;
}
#Override
protected boolean matches(final ObjectNode objectNode) {
return objectNode.has("numCylinders");
}
};
// Enum.values() returns array clones every time it's invoked
private static final BaseVehicleTypeResolver[] cachedBaseVehicleTypeResolvers = BaseVehicleTypeResolver.values();
protected abstract Class<? extends BaseVehicle> getBaseVehicleClass();
protected abstract boolean matches(ObjectNode objectNode);
}
}
As you can see, such an approach is more or less fragile and sophisticated, but it tries to make some analysis. Now, how it can be used:
final ObjectMapper mapper = new ObjectMapper();
Stream.of(
"{\"orgId\":\"foo\",\"isInitialized\":true,\"vehicle\":{\"make\":\"foo\",\"model\":\"foo\"}}",
"{\"orgId\":\"bar\",\"isInitialized\":true,\"vehicle\":{\"make\":\"bar\",\"model\":\"bar\",\"numCylinders\":4}}"
)
.map(json -> {
try {
return mapper.readValue(json, Payload.class);
} catch ( final IOException ex ) {
throw new RuntimeException(ex);
}
})
.map(Payload::getVehicle)
.map(BaseVehicle::getClass)
.forEach(System.out::println);
Output:
class q43138817.Car
class q43138817.Motorcycle

Related

How deserialize plain String to Json using Jackson in Java?

I have a simple class as property of mage:
// getter/setter omitted for brevity
public class Magic() {
String Spell;
int strength;
}
public class Mage() {
String name;
Magic magic;
}
I need to deserialize JSON from 2 different source strings:
{
"name" : "Sauron",
"magic" : {
"spell" : "Tamador",
"strenght" : 10
}
}
and
{
"name" : "Gandalf",
"magic" : "You shall not pass"
}
or even "You shall not pass" -> Magic object
I thought going with #JsonDeserialize(using = MagicDeserializer.class) would be the way to go with Jackson, but the Parser barfs with "Unrecognized token". Is there a way I can intercept the loading to do my own parsing?
The idea of a custom deserializer is correct, you can extends the StdDeserializer class and in its deserialize method convert the json to a JsonNode separating the two Stringand Object distinct values associated to the magic key in the json:
public class MagicDeserializer extends StdDeserializer<Magic> {
public MagicDeserializer() {
super(Magic.class);
}
#Override
public Magic deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
final ObjectCodec codec = jp.getCodec();
JsonNode root = codec.readTree(jp);
Magic magic = new Magic();
if (root.isTextual()) { //<- magic is a string
magic.setSpell(root.textValue());
return magic;
}
//ok, so magic is an Magic object
return codec.treeToValue(root, Magic.class);
}
}
Then if you annotate your Magic field you can deserialize both the jsons:
#Data
public class Mage {
private String name;
#JsonDeserialize(using = MagicDeserializer.class)
private Magic magic;
}
#Data
public class Magic {
private String Spell;
private int strength;
}
Mage sauron = mapper.readValue(json1, Mage.class);
System.out.println(mapper.writeValueAsString(sauron));
Mage gandalf = mapper.readValue(json2, Mage.class);
System.out.println(mapper.writeValueAsString(gandalf));

Automatic deserialization of String to Object with Jackson

Context
Say you have:
public class Dto {
private String name;
private String List<Custom> customs;
// getters and setters...
}
and
public class Custom {
private String something;
private String else;
// getters and setters...
}
Your Spring MVC RestController receives a list of Dto:
#PostMapping
public String create(#RequestBody #Valid List<Dto> dtos) {
return myService.process(features);
}
Input
However, you know that the client-side service which will send data to your controller will send something like this:
[
{
"name": "Bob",
"customs": [
"{\n \"something\": \"yes\",\n \"else\": \"no\"\n }"
]
}
]
Notice how the List<Custom> actually ends up being received as a List<String>. Please assume this cannot be changed on the client-side and we have to deal with it on the server-side.
Question
Is there a Jackson annotation which would automagically take the input String and try to serialize it into a Custom class?
Attempts
A few things that didn't work, including:
#JsonSerialize(using = ToStringSerializer.class)
private List<Custom> customs;
along with
public Custom(String json) {
try {
new ObjectMapper().readerFor(Custom.class).readValue(json);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
As it is, we have had to change the customs type to List<String> and add a utility method which converts a String into a Custom using an ObjectMapper. This is rather dissatisfying.
You need to implement custom deserialiser or converter which would be used to convert given payload to required type. One trick, you could use is to create new ObjectMapper and use it for internal deserialisation.
Example usage:
class CustomConverter extends StdConverter<String, Custom> {
private final ObjectMapper mapper = new ObjectMapper();
#Override
public Custom convert(String value) {
try {
return mapper.readValue(value, Custom.class);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(value);
}
}
}
class Dto {
private String name;
#JsonDeserialize(contentConverter = CustomConverter.class)
private List<Custom> customs;
}
You need to create a custom Deserializer.
public class CustomDeserializer extends StdDeserializer<Custom> {
public CustomDeserializer() {
this(null);
}
public CustomDeserializer(Class<?> vc) {
super(vc);
}
#Override
public Custom deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
int id = (Integer) ((IntNode) node.get("id")).numberValue();
String name = node.get("name").asText();
...
return new Custom(id, name, ...);
}
}
and register the deserializer on the Custom class:
#JsonDeserialize(using = CustomDeserializer.class)
public class Custom {
...
}

Parse Json into POJO using Jackson

I have the following json
{
"root": {
"status": "UP",
"connection1": {
"status": "UP"
},
"connection2": {
"status": "UP"
}
}
}
Also i have the following POJO classes i want to convert JSON into
#JsonIgnoreProperties(ignoreUnknown = true)
public class POJO {
#JsonProperty("root")
#JsonDeserialize(using = RootDeserializer.class)
private Root root;
//getters + setters
}
public class Root {
private boolean isAlive;
private List<Connection> connections;
public Root(boolean isAlive, List<Connection> connections) {
this.isAlive = isAlive;
this.connections = connections;
}
//getters + setters
}
#JsonIgnoreProperties(ignoreUnknown = true)
public class Connection {
private String status;
//getters + setters
}
And finally i have this deserializer to convert json into Root instance
public class RootDeserializer extends JsonDeserializer<Root> {
private static final String CONNECTION_PREFIX = "connection";
private static final String UP_STATUS = "UP";
private ObjectMapper objectMapper = new ObjectMapper();
#Override
public Root deserialize(JsonParser parser, DeserializationContext context) throws IOException {
Map<String, Map<String, Object>> rootJsonMap = parser.readValueAs(Map.class);
boolean isAlive = StringUtils.equals(UP_STATUS, String.valueOf(rootJsonMap.get("status")));
List<Connection> connections = rootJsonMap.entrySet()
.stream()
.filter(entry -> StringUtils.startsWithIgnoreCase(entry.getKey(), CONNECTION_PREFIX))
.map(this::mapToConnection)
.collect(Collectors.toList());
return new Root(isAlive, connections);
}
private PosServerConnection mapToConnection(Map.Entry<String, Map<String, Object>> entry) {
Map<String, Object> connectionJsonMap = entry.getValue();
return objectMapper.convertValue(connectionJsonMap, Connection.class);
}
}
This way i can group all my Connections into one List in Root class.
My question is there any another way to do this ??
I'd like to do this without such big deserializer using just Jackson annotations on my Pojo classes
You can simply achieve this by using #JsonAnySetter annotation for customizing Setter for List<Connection> as follows. You can also reference to Jackson Annotation Examples to see how it works.
POJOs
public class Pojo {
private Root root;
//general getters, setters and toString
}
public class Root {
private String status;
private List<Connection> connections = new ArrayList<>();
public List<Connection> getConnections() {
return connections;
}
#JsonAnySetter
public void setConnections(String name, Connection connection) {
connection.setName(name);
this.connections.add(connection);
}
//other getters, setters and toString
}
public class Connection {
private String name;
private String status;
//general getters, setters and toString
}
Then you can serialize the given JSON string to Pojo with common way by Jackson:
Code Snippet
ObjectMapper mapper = new ObjectMapper();
Pojo pojo = mapper.readValue(jsonStr, Pojo.class);
System.out.println(pojo.getRoot().getConnections().toString());
Console output
[Connection [name=connection1, status=UP], Connection [name=connection2, status=UP]]

How to deserialize JSON to interface?

I have trouble with deserialization JSON to some of classes ChildA, ChildB and etc. that implements Basic interface in following example.
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = InstagramUser.class, name = "ChildA")
})
public interface Basic {
getName();
getCount();
}
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonTypeName("ChildA")
public class ChildA implements Basic { ... }
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonTypeName("ChildB")
public class ChildB implements Basic { ... }
...
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonIgnoreProperties(ignoreUnknown = true)
public class Response<E extends Basic> {
#JsonProperty("data")
private List<E> data;
public List<E> getData() {
return data;
}
public void setData(List<E> data) {
this.data = data;
}
}
// deserialization
HTTPClient.objectMapper.readValue(
response,
(Class<Response<ChildA>>)(Class<?>) Response.class
)
Exception is: com.fasterxml.jackson.databind.JsonMappingException: Unexpected token (END_OBJECT), expected FIELD_NAME: missing property 'type' that is to contain type id (for class Basic)
Expected JSON is like this:
{
"data": [{ ... }, ...]
}
There is no property that is presented in all type objects so they are completely different. But as you can see on readValue line I know what is expected type. How to structure JsonTypeInfo and JsonSubTypes annotaions to deserialize JSON as expected class?
I kinda had the same problem as you, based in the reading here: Jackson Deserialize Abstract Classes I created my own solution, it basically consists of creating my own deserializer, the trick is to use/identify a specific property within JSON to know which instance type should be returned from deserialization, example is:
public interface Basic {
}
First Child:
public class ChildA implements Basic {
private String propertyUniqueForThisClass;
//constructor, getters and setters ommited
}
SecondChild:
public class ChildB implements Basic {
private String childBUniqueProperty;
//constructor, getters and setters ommited
}
The deserializer (BasicDeserializer.java) would be like:
public class BasicDeserializer extends StdDeserializer<Basic> {
public BasicDeserializer() {
this(null);
}
public BasicDeserializer(final Class<?> vc) {
super(vc);
}
#Override
public Basic deserialize(final JsonParser jsonParser,
final DeserializationContext deserializationContext)
throws IOException {
final JsonNode node = jsonParser.getCodec().readTree(jsonParser);
final ObjectMapper mapper = (ObjectMapper) jsonParser.getCodec();
// look for propertyUniqueForThisClass property to ensure the message is of type ChildA
if (node.has("propertyUniqueForThisClass")) {
return mapper.treeToValue(node, ChildA.class);
// look for childBUniqueProperty property to ensure the message is of type ChildB
} else if (node.has("childBUniqueProperty")) {
return mapper.treeToValue(node, ChildB.class);
} else {
throw new UnsupportedOperationException(
"Not supported class type for Message implementation");
}
}
}
Finally, you'd have an utility class (BasicUtils.java):
private static final ObjectMapper MAPPER;
// following good software practices, utils can not have constructors
private BasicUtils() {}
static {
final SimpleModule module = new SimpleModule();
MAPPER = new ObjectMapper();
module.addDeserializer(Basic.class, new BasicDeserializer());
MAPPER.registerModule(module);
}
public static String buildJSONFromMessage(final Basic message)
throws JsonProcessingException {
return MAPPER.writeValueAsString(message);
}
public static Basic buildMessageFromJSON(final String jsonMessage)
throws IOException {
return MAPPER.readValue(jsonMessage, Basic.class);
}
For testing:
#Test
public void testJsonToChildA() throws IOException {
String message = "{\"propertyUniqueForThisClass\": \"ChildAValue\"}";
Basic basic = BasicUtils.buildMessageFromJSON(message);
assertNotNull(basic);
assertTrue(basic instanceof ChildA);
System.out.println(basic);
}
#Test
public void testJsonToChildB() throws IOException {
String message = "{\"childBUniqueProperty\": \"ChildBValue\"}";
Basic basic = BasicUtils.buildMessageFromJSON(message);
assertNotNull(basic);
assertTrue(basic instanceof ChildB);
System.out.println(basic);
}
The source code can be found on: https://github.com/darkstar-mx/jsondeserializer
I find not exactly solution but a workaround. I used custom response class ChildAResponse and passed it to ObjectMapper.readValue() method.
class ChildAResponse extends Response<ChildA> {}
// deserialization
HTTPClient.objectMapper.readValue(
response,
ChildAResponse.class
)
So JsonTypeInfo and JsonSubTypes annotations on the interface are no longer needed.

Jackson mapping Object or list of Object depending on json input

I have this POJO :
public class JsonObj {
private String id;
private List<Location> location;
public String getId() {
return id;
}
public List<Location> getLocation() {
return location;
}
#JsonSetter("location")
public void setLocation(){
List<Location> list = new ArrayList<Location>();
if(location instanceof Location){
list.add((Location) location);
location = list;
}
}
}
the "location" object from the json input can be either a simple instance of Location or an Array of Location. When it is just one instance, I get this error :
Could not read JSON: Can not deserialize instance of java.util.ArrayList out of START_OBJECT token
I've tried to implement a custom setter but it didn't work. How could I do to map either a Location or a List depending on the json input?
Update: Mher Sarkissian's soulution works fine, it can also be used with annotations as suggested here, like so:.
#JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
private List<Item> item;
My deepest sympathies for this most annoying problem, I had just the same problem and found the solution here: https://stackoverflow.com/a/22956168/1020871
With a little modification I come up with this, first the generic class:
public abstract class OptionalArrayDeserializer<T> extends JsonDeserializer<List<T>> {
private final Class<T> clazz;
public OptionalArrayDeserializer(Class<T> clazz) {
this.clazz = clazz;
}
#Override
public List<T> deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
ObjectCodec oc = jp.getCodec();
JsonNode node = oc.readTree(jp);
ArrayList<T> list = new ArrayList<>();
if (node.isArray()) {
for (JsonNode elementNode : node) {
list.add(oc.treeToValue(elementNode, clazz));
}
} else {
list.add(oc.treeToValue(node, clazz));
}
return list;
}
}
And then the property and the actual deserializer class (Java generics is not always pretty):
#JsonDeserialize(using = ItemListDeserializer.class)
private List<Item> item;
public static class ItemListDeserializer extends OptionalArrayDeserializer<Item> {
protected ItemListDeserializer() {
super(Item.class);
}
}
This is already supported by jackson
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);

Categories