Polymorphic Deserialization Issue with Jackson - java

I have the following classes that I want to deserialize a JSON string to using Jackson.
PushNotificationMessage.java
public class PushNotificationMessage {
#JsonProperty("device_info")
private DeviceInfo deviceInfo;
private String content;
//getters & setters
}
DeviceInfo.java
public class DeviceInfo {
#JsonProperty(value = "device_type")
private String deviceType;
//getters & setters
}
IOSDeviceInfo.java
public class IOSDeviceInfo extends DeviceInfo {
#JsonProperty(value = "device_id")
private String deviceId;
private String arn;
#JsonProperty(value = "user_data")
private String userData;
//getters & setters
}
WebDeviceInfo.java
public class WebDeviceInfo extends DeviceInfo {
private String endpoint;
private String key;
private String auth;
//getters & setters
}
I have the following JSON content that I want to deserialize:
{
"device_info": {
"endpoint": "https://android.googleapis.com/gcm/send/blah",
"key": "blahkey",
"auth": "blahauth",
"device_type": "web"
},
"content": "Notification content"
}
I simply use ObjectMapper to try to perform the deserialization as such.
final ObjectMapper objectMapper = new ObjectMapper();
final PushNotificationMessage message = objectMapper.readValue(jsonString, PushNotifictionMessage.class);
When I do this I get:
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "endpoint" (class com.blah.DeviceInfo), not marked as ignorable (one known property: "device_type"])
How can I get Jackson to recognize that it needs to be mapped to a WebDeviceInfo instance, instead of trying to map it to the DeviceInfo superclass, which does not have the endpoint field?
I've tried playing with #JsonTypeInfo and #JsonSubTypes annotations in my different classes, but I can find no good examples of how to use them.
EDIT: I added the #JsonDeserialize(using = DeviceInfoDeserializer.class) annotation to my DeviceInfo class, and created the following DeviceInfoDeserializer.
public class DeviceInfoDeserializer extends JsonDeserializer<DeviceInfo> {
private static final String DEVICE_TYPE = "device_type";
private static final String WEB = "web";
private static final String IOS = "ios";
#Override
public DeviceInfo deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) throws IOException {
final ObjectMapper objectMapper = (ObjectMapper) jsonParser.getCodec();
final ObjectNode root = objectMapper.readTree(jsonParser);
if (root.has(DEVICE_TYPE)) {
final JsonNode jsonNode = root.get(DEVICE_TYPE);
if (jsonNode.asText().equalsIgnoreCase(WEB)) {
return objectMapper.readValue(root.toString(), WebDeviceInfo.class);
} else if (jsonNode.asText().equalsIgnoreCase(IOS)) {
return objectMapper.readValue(root.toString(), IOSDeviceInfo.class);
}
}
throw deserializationContext.mappingException("Failed to de-serialize device info, as device_type was not \"web\" or \"ios\"");
}
}
Now, I get a different error when attempting to deserialize my PushNotificationMessage JSON:
java.lang.StackOverflowError: null
at com.fasterxml.jackson.databind.deser.std.BaseNodeDeserializer.deserializeObject(JsonNodeDeserializer.java:210)
at com.fasterxml.jackson.databind.deser.std.JsonNodeDeserializer.deserialize(JsonNodeDeserializer.java:69)
at com.fasterxml.jackson.databind.deser.std.JsonNodeDeserializer.deserialize(JsonNodeDeserializer.java:15)
at com.fasterxml.jackson.databind.ObjectMapper._readValue(ObjectMapper.java:3770)
at com.fasterxml.jackson.databind.ObjectMapper.readTree(ObjectMapper.java:2207)
at com.blah.serialization.DeviceInfoDeserializer.deserialize(DeviceInfoDeserializer.java:25)
at com.blah.serialization.DeviceInfoDeserializer.deserialize(DeviceInfoDeserializer.java:16)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3798)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2842)
... (above trace repeated many times)
EDIT: Just needed to add #JsonDeserialize(as = WebDeviceInfo.class) and #JsonDeserialize(as = IOSDeviceInfo.class) to my subclasses, now it works as expected. Big thank you to #Luciano van der Veekens.

Jackson is not aware of polymorphism, it just tries to create an instance of the concrete DeviceInfo class.
However, you can implement a custom deserializer that programmatically parses the device info JSON and knows when to instantiate one of the subclasses due to the uniqueness of some fields such as endpoint.
https://fasterxml.github.io/jackson-databind/javadoc/2.2.0/com/fasterxml/jackson/databind/annotation/JsonDeserialize.html
#JsonDeserialize(using = DeviceInfoDeserializer.class)
public class DeviceInfo {
}
An example can be found here: http://sunilkumarpblog.blogspot.nl/2015/12/javajson-polymorphic-serialization-de.html

Related

Jackson include a key that is empty string

I accept from a server a json like this:
{
"": "hello"
}
And in Jackson I did
#JsonProperty("")
private String string
When deserialising the object it ignores the property completely.
How can I make an empty string count as a key?
Thank you
I found a way to achieve what you want with custom deserializer by following steps.
Step 1: Create the POJO class you want to deserialize to
public class MyPojo {
private String emptyFieldName;
//constructor, getter, setter and toString
}
Step 2: Create your custom deserializer
public class MyDeserializer extends StdDeserializer<MyPojo> {
public MyDeserializer () {
this(null);
}
protected MyDeserializer (Class<?> vc) {
super(vc);
}
#Override
public MyObject deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser);
String emptyFieldName = jsonNode.get("").asText();
return new MyPojo(emptyFieldName);
}
}
Step 3: Register this custom deserializer
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(MyPojo.class, new MyDeserializer());
objectMapper.registerModule(module);
MyPojo myPojo = objectMapper.readValue(jsonStr, MyPojo.class);
System.out.println(myPojo.getEmptyFieldName());
Console output:
hello
BTW, you could also directly register this custom deserializer on the class:
#JsonDeserialize(using = MyDeserializer.class)
public class MyPojo {
...
}
For more information, please refer to Getting Started with Custom Deserialization in Jackson.

Java: How to Make certain JSON field not to be printed

I have a Document request that I need to send to a REST service endpoint:
#Data // lombok
public class Document {
private String name;
private String location;
private String content; // a base64 encoded byte array
}
I have a utility class that I can use to log the entire JSON object in the log file. So in case an exception is thrown at runtime, the code will log at the ERROR level the request that causes the exception, something like this:
{
"name": "file1.pdf",
"location" : "/root/folder/",
"content" : "JVBERi0xLjQKJdP0zOEKJxxxxxxxxxxxxxxxxxxxx"
}
The problem is in the "content" field. If the file is very large, the "content" field will fill up the log file very quickly when an exception happens at runtime. So how do I make the "content" field not to be printed in the log file? I thought about using #JsonIgnore, but will it make the "content" field to be ignored when the request is passed to the endpoint? I am not using Gson.
You can use JacksonMixin:
#Data
public class Document {
private String name;
private String location;
private String content;
}
public static abstract class ContentIgnoreMixin {
#JsonIgnore
private String content;
}
// same as ContentIgnoreMixin, but interface using getter
public interface class ContentIgnoreMixin2 {
#JsonIgnore
String getContent();
}
and later:
ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(Document.class, ContentIgnoreMixin.class); // or ContentIgnoreMixin2 - no difference
String jsonWithoutContent = mapper.writeValueAsString(objectWithContent);

how to tell jackson to put unrecognized json data into a particular field while deserializing

My data model/POJO:
public class MyPojo {
#JsonProperty("description")
protected String content;
#JsonProperty("name")
protected String title;
#JsonProperty("property")
protected List<Property> property;
#JsonProperty("documentKey")
protected String iD;
// Getters and setters...
}
However, my server returns json response in the following format.
{
"documentKey": "J2D2-SHRQ1_2-55",
"globalId": "GID-752726",
"name": "SHReq - Textual - heading test",
"description": "some text",
"status": 292,
"rationale$58": "Value of rationale",
"remark": "Just for testing purposes",
"release": 203
}
Here I've mapped the documentKey to iD and name to title of MyPojo. However, while using jackson's ObjectMapper, I get an exception saying globalId isn't recongnized.
The problem here is that it should put all such data fields (globalId, status, remark, release etc.) into the list of properties(List<Property> property). So I shouldn't tell jackson to ignore those.
How can I do that?
I think you will need to use a custom Deserializer. This way you have total control on how to arrange the data
class MyPojoDeserializer extends StdDeserializer<MyPojo> {
public MyPojoDeserializer() {
this(null);
}
public MyPojoDeserializer(Class<?> vc) {
super(vc);
}
#Override
public MyPojo deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
JsonNode node = jp.getCodec().readTree(jp);
MyPojo myPojo = new MyPojo();
myPojo.setId(node.get("documentKey").asText());
myPojo.setContent(node.get("documentKey").asText());
myPojo.setTitle(node.get("name").asText());
// I just make a list of Strings here but simply change it to make a List<Property>,
// I do not know which Property class you want to use
List<String> properties = new ArrayList<>();
properties.add(node.get("globalId").asText());
properties.add(node.get("status").asText());
properties.add(node.get("rationale$58").asText());
properties.add(node.get("remark").asText());
properties.add(node.get("release").asText());
myPojo.setProperty(properties);
return myPojo;
}
}
Then add the following annotation to your MyPojo class
#JsonDeserialize(using = MyPojoDeserializer.class)
public class MyPojo {
protected String id;
protected String content;
protected String title;
protected List<Property> property;
// Getters/Setters
}
Then a classic readValue call should work
MyPojo myPojo = new ObjectMapper().readValue(json, MyPojo.class);

MethodArgumentConversionNotSupportedException when I try to map json string onto java domain class in Spring controller's method

From the frontend I receive GET-request which contains encoded json string as one of its parameters:
http://localhost:8080/engine/template/get-templates?context=%7B%22entityType%22%3A%22DOCUMENT%22%2C%22entityId%22%3A%22c7a2a0c6-fd34-4f33-9cb8-14c2090565ea%22%7D&page=1&start=0&limit=25
Json-parameter 'context' without encoding (UUID is random):
{"entityType":"DOCUMENT","entityId":"c7a2a0c6-fd34-4f33-9cb8-14c2090565ea"}
On backend my controller's method which handle that request looks like this:
#RequestMapping(value = "/get-templates", method = RequestMethod.GET)
public List<Template> getTemplates(#RequestParam(required = false, name = "context") Context context) {
//...
}
'Context' domain class:
public class Context {
private String entityType;
private UUID entityId;
public String getEntityType() {
return entityType;
}
public void setEntityType(String entityType) {
this.entityType = entityType;
}
public UUID getEntityId() {
return entityId;
}
public void setEntityId(UUID entityId) {
this.entityId = entityId;
}
}
I believed Spring's Jackson module would automatically convert that kind of json to java object of Context class, but when I run this code it gives me exception:
org.springframework.web.method.annotation.MethodArgumentConversionNotSupportedException: Failed to convert value of type 'java.lang.String' to required type 'com.company.domain.Context'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'com.company.domain.Context': no matching editors or conversion strategy found
On StackOverflow I've seen similar questions, but those were about POST-requests handling (with #RequestBody annotation), which doesn't fit with GET-request.
Could you help me to solve this problem?
Thanks in advance.
I think you need to specify that your GET mapping is looking to consume JSON:
#RequestMapping(value = "/get-templates", method = RequestMethod.GET, consumes = "application/json")
public List<Template> getTemplates(#RequestParam(required = false, name = "context") Context context) {
//...
}
If this doesn't work then you can call the Jackson ObjectMapper yourself:
#RequestMapping(value = "/get-templates", method = RequestMethod.GET)
public List<Template> getTemplates(#RequestParam(required = false, name = "context") String context) {
ObjectMapper mapper = new ObjectMapper();
Context myContext = mapper.readValue(context, Context.class);
//...
}
As far as I know Spring does not have the mechanism to convert from a String to a UUID in older releases. In such case you should declare you entityId as a String and then use a converter in order to convert it to UUID.
So your Context class should be like below:
public class Context {
private String entityType;
private String entityId;
public String getEntityType() {
return entityType;
}
public void setEntityType(String entityType) {
this.entityType = entityType;
}
public String getEntityId() {
return entityId;
}
public void setEntityId(String entityId) {
this.entityId = entityId;
}
public UUID getEntityIdAsUUID() {
return convertToUUID(this.entityId);
}
// Helper Conversion String to UUID method
private UUID convertToUUID(String entityId){
return UUID.fromString(entityId);
}
}
I faced the same issue in both Jersey and Spring MVC when trying to convert the JSON String {"x":"1001822.831","y":"200716.8913"} to a object of a class called Point
Point class is as below
public class Point
{
private Double x;
private Double y;
//getters and setters
}
As per Jersey documentation, I added the below method to Point class and it worked for both Jersey and Spring MVC.
//used by jax rs & spring mvc for converting queryParam String to Point
public static Point valueOf(String json) throws IOException
{
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(json, Point.class);
}
Please refer to section 3.2 here https://jersey.github.io/documentation/latest/jaxrs-resources.html#d0e2271

How can I get a reader method based on a json property name using jackson?

Given the following jackson annotated class :
public class AClass {
#JsonProperty("propertyName")
private String anyProperty
public String getAnyProperty() {
...
}
...
}
or a mixin configuration:
public class AClass {
private String anyProperty
public String getAnyProperty() {
...
}
...
}
public interface AClassMixin {
#JsonProperty(value = "propertyName")
String getAnyProperty();
}
How can I get the json property "propertyName' reader method using jackson?
I need something like that:
ObjectMapper mapper = new ObjectMapper();
Method method = mapper.getReaderMethodForProperty("propertyName", Aclass.class);
Construct a JavaType for your bean class
JavaType target = objectMapper.constructType(AClass.class);
then use the ObjectMapper's DeserializationConfig to introspect it. This will give you a BeanDescription.
BeanDescription beanDescription = objectMapper.getDeserializationConfig().introspect(target)
You can use that to get a list of its BeanPropertyDefinition instances.
List<BeanPropertyDefinition> beanPropertyDefinitions = beanDescription.findProperties();
Each BeanPropertyDefinition has methods to retrieve getters and setters (and other things) as AnnotatedMember values from which you can retrieve the Member (you'll need to cast to Method).
for (BeanPropertyDefinition bpd : beanPropertyDefinitions) {
AnnotatedMember annotatedMember = bpd.getAccessor();
Member member = annotatedMember.getMember();
if (member instanceof Method) {
Method getterMethod = (Method) member;
System.out.println(getterMethod.getName());
}
}

Categories

Resources