Custom deserializer for any list in Jackson - java

I have a problem with wrong objects in lists. For instance I've a JSON model:
{
"items": [
{
"id": 1,
"name": "Item1"
},
{
"id": 2,
"name": "Item2"
},
{
"id": [],
"name": "Item3"
}
]
}
and two POJO
data class BadList(val items: List<BadItem>)
data class BadItem(val id: Int, val name: String)
Of course, when the parser stumbles upon a third element I get the exception
com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.lang.Integer out of START_ARRAY token
at [Source: {"items":[{"id":1,"name":"Item1"},{"id":2,"name":"Item2"},{"id":[],"name":"Item3"}]}; line: 1, column: 19] (through reference chain: my.package.BadList["items"]->java.util.ArrayList[2]->my.package.BadItem["id"])
Who knows how to get around this? I want to skip that wrong item.

You can write a custom deserializer and implement deserialization logic in it, e.g.:
class ItemIdDeserialiser extends JsonDeserializer<Integer> {
#Override
public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Object value = p.getCurrentValue();
//Check if it's Integer
if(value instanceof Integer){
return (Integer) value;
}
return null; //Or return first element if it's a non empty list
}
}
Once this is done, you can annotate the field with #JsonDeserialise to instruct jackson to use your class, e.g.:
class Item {
#JsonDeserialize(using = ItemIdDeserialiser.class)
private Integer id;
}
Update
If you just want to ignore the field in serialization/deserialization then you can annotate it with #JsonIgnore, e.g.
class Item {
#JsonIgnore
private Integer id;
}
Or even better, remove id from pojo and add #JsonIgnoreProperties on the class, e.g.:
#JsonIgnoreProperties(ignoreUnknown = true)
class Item {
}
It will automatically ignore the properties which are present in json but not found in class.

You can use a "HidableSerializer" for this and check the data during serialization
1. Create a IHidable interface
The interface has a isHidden method which is called during serialization
package ch.hasselba.jackson.test;
public interface IHidable {
public boolean isHidden();
}
2. Change your BadItem class
Add the interface and change the setter of id. When property id is deserialized, it is tested if it is an Integer. If not, the item is marked as bad.
package ch.hasselba.jackson.test;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
#JsonIgnoreProperties( {"hidden"} )
public class BadItem implements IHidable{
private Integer id;
public String name;
private boolean isBadItem;
public Integer getId(){
return id;
}
public void setId(Object value){
if( value instanceof Integer ){
this.id = (Integer) value;
}else{
this.isBadItem = true;
}
}
public boolean isHidden() {
return isBadItem;
}
}
3. Create a HidableSerializer
package ch.hasselba.jackson.test;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
#SuppressWarnings("serial")
public class HidableSerializer<T> extends StdSerializer<T> {
private JsonSerializer<T> defaultSerializer;
protected HidableSerializer(Class<T> t) {
super(t);
}
public JsonSerializer<T> getDefaultSerializer() {
return defaultSerializer;
}
public void setDefaultSerializer(JsonSerializer<T> defaultSerializer) {
this.defaultSerializer = defaultSerializer;
}
#Override
public void serialize(T value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException {
if( value instanceof IHidable ){
IHidable hidableValue = (IHidable) value;
if( hidableValue.isHidden() )
return;
}
defaultSerializer.serialize(value, jgen, provider);
}
}
4. Register the HidableSerializer and that's it
package ch.hasselba.jackson.test;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
public class Demo {
#SuppressWarnings("serial")
public static void main(String[] args) {
// register the HidableSerializer
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(Include.NON_EMPTY);
mapper.registerModule(new SimpleModule() {
#Override
public void setupModule(SetupContext context) {
super.setupModule(context);
context.addBeanSerializerModifier(new BeanSerializerModifier() {
#Override
public JsonSerializer<?> modifySerializer(
SerializationConfig config, BeanDescription desc, JsonSerializer<?> serializer) {
if (BadItem.class.isAssignableFrom(desc.getBeanClass())) {
HidableSerializer ser = new HidableSerializer(BadItem.class);
ser.setDefaultSerializer(serializer);
return ser;
}
return serializer;
}
});
}
});
String content = "{ \"items\": [ { \"id\": 1, \"name\": \"Item1\" }, { \"id\": 2, \"name\": \"Item2\" }, { \"id\":[], \"name\": \"Item3\" } ]}";
// build the Object
BadList test = null;
try {
test = mapper.readValue(content, BadList.class);
} catch (Exception e) {
e.printStackTrace();
}
// and now convert it back to a String
String data = null;
try {
data = mapper.writeValueAsString(test);
} catch (Exception e) {
e.printStackTrace();
}
// print the result
System.out.println( data );
}
}
When changing the id "[]" to an Integer value, the Item is displayed, otherwise it is empty.
The result:
{"items":[{"id":1,"name":"Item1"},{"id":2,"name":"Item2"}]}

Related

can create custom json string with instantiated data class in java

There is a class defined follows:
#Data // lombok
public class MyData {
#Required // my custom annotation
String testValue1;
Integer testValue2;
}
And myData is instantiated like that:
MyData myData = new MyData();
myData.setTestValue1("test1");
myData.setTestValue2(123);
I want to serialize myData as json string as follows:
{
"testValue1": {
"type": "String",
"isRequired": "true",
"value": "test1"
},
"testValue2": {
"type": "Integer",
"isRequired": "false",
"value": "123"
},
}
Is there a good way to create json string?
edit|
I put quotes on json string that to be able to valid.
I want to set key as field name and create additional field information.
set field type on "type" key and
if field has #Required annotation, set true on "isRequired" and
set instantiated field value on "value".
So I played a bit around with Jackson Serialization and came to this result (certainly unfinished and not fully tested, but works with your given object).:
Module to make Spring / Jackson known of the new Serializer.
#JsonComponent
public class TestSerializerModule extends SimpleModule {
#Override
public String getModuleName() {
return TestSerializerModule.class.getSimpleName();
}
#Override
public Version version() {
return new Version(
1,
0,
0,
"",
TestSerializerModule.class.getPackage().getName(),
"TestModule"
);
}
#Override
public void setupModule(SetupContext context) {
context.addBeanSerializerModifier(new BeanSerializerModifier() {
#Override
public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
if (beanDesc.getBeanClass().equals(MyData.class)) { //Add some smart logic here to identify your objects
return new TestSerializer();
}
return serializer;
}
});
}
}
Then the Serialisier itself:
public class TestSerializer extends StdSerializer<Object> {
protected TestSerializer() {
super(Object.class);
}
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
ClassIntrospector classIntrospector = provider.getConfig().getClassIntrospector();
BasicBeanDescription beanDescription = (BasicBeanDescription) classIntrospector.forSerialization(provider.getConfig(), provider.constructType(value.getClass()), null);
// Start of the MyValue Object
gen.writeStartObject();
beanDescription.findProperties().forEach(p -> {
// Requiered if Annoation is present
boolean required = p.getField().hasAnnotation(Required.class);
try {
// Write all the wanted fields
gen.writeFieldName(p.getName());
gen.writeStartObject();
gen.writeBooleanField("isRequired", required);
gen.writeStringField("type", p.getField().getRawType().getSimpleName());
gen.writeFieldName("value");
Object value1 = p.getGetter().getValue(value);
// Use existing serializer for the value provider.findValueSerializer(value1.getClass()).serialize(value1, gen, provider);
gen.writeEndObject();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
);
gen.writeEndObject();
}
}
Running this test :
#JsonTest
class TestSerializerTest {
#Autowired
ObjectMapper objectMapper;
#Test
public void testSerializer() throws Exception {
MyData value = new MyData();
value.setTestValue1("test1");
value.setTestValue2(123);
String s = objectMapper.writeValueAsString(value);
System.out.println(s);
}
}
gives me this output:
{"testValue1":{"isRequired":false,"type":"String","value":"test1"},"testValue2":{"isRequired":false,"type":"Integer","value":123}}
Hope that gives you an idea where to start and how to proceed from here!

Two differents types of responses in POJO

I'm using Retrofit with POJO, which usually works, but the answer has two different objects depending on whether the result is valid. Which one is a String and the another is an Object:
{
"data":"No results."
}
or:
{
"data": {
"exame": [
{
"id": 776,
"codigo": "DHT",
"instrucao": "- Text."
},
{
"id": 776,
"codigo": "DHT",
"instrucao": "- Text"
}
]
}
}
And my class:
public class Exame {
#SerializedName("data")
#Expose
#Nullable
private ExameItem exameItem;
private String mensagem;
public ExameItem getExameItem() {
return exameItem;
}
public void setExameItem(ExameItem exameItem) {
this.exameItem = exameItem;
}
public String getMensagem() {
return mensagem;
}
public void setMensagem(String mensagem) {
this.mensagem = mensagem;
}
}
When I do Exame().getExameItem its fine, but when I try test if have a message in Exame().getMessagem its bring me a this error:
java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 10 path $.data
So, I think how can I test if #data is a String of an Object, but I don't kwon how, anyone may help?
You need to implement custom deserialiser by implementing JsonDeserializer interface. See below example:
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.JsonAdapter;
import java.io.File;
import java.io.FileReader;
import java.lang.reflect.Type;
import java.util.List;
public class GsonApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
Gson gson = new GsonBuilder().create();
System.out.println(gson.fromJson(new FileReader(jsonFile), Exame.class));
}
}
class ExamsJsonDeserializer implements JsonDeserializer<Exame> {
#Override
public Exame deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject root = json.getAsJsonObject();
JsonElement data = root.get("data");
Exame exam = new Exame();
if (data.isJsonPrimitive()) {
exam.setMensagem(data.getAsString());
} else {
ExameItem examItem = context.deserialize(data, ExameItem.class);
exam.setExameItem(examItem);
}
return exam;
}
}
#JsonAdapter(ExamsJsonDeserializer.class)
class Exame {
private ExameItem exameItem;
private String mensagem;
// getters, setters, toString
}
class ExameItem {
private List<Item> exame;
//getters, setters, toString
}
class Item {
private int id;
// ...
//getters, setters, toString
}

Serialize and Deserialize object contains Map type which do not wrapper with property name, such as Baidu Push Notification IOS Message

I am trying to play with the Baidu Push Notification RESTFUL API, however, I failed to figure out how to serialize and deserialize IOS Message object by Jackson with annotation.
Json Example of Target IOS Message
{
"aps": {
"alert":"Message From Baidu Cloud Push-Service",
"sound":"", //可选
"badge":0, //可选
},
"key1":"value1",
"key2":"value2"
}
"key1":"value1" and "key2":"value2" comes from a Map.
My IosApsMessage object
import com.fasterxml.jackson.annotation.JsonProperty;
public class IosApsMessage {
#JsonProperty("alert")
private String alert; //REQUIRED
#JsonProperty("sound")
private String sound;
#JsonProperty("badge")
private Integer badge;
public String getAlert() {
return alert;
}
public void setAlert(String alert) {
this.alert = alert;
}
public String getSound() {
return sound;
}
public void setSound(String sound) {
this.sound = sound;
}
public Integer getBadge() {
return badge;
}
public void setBadge(Integer badge) {
this.badge = badge;
}
}
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
public class IosNotificationMessage {
#JsonProperty("aps")
private IosApsMessage aps;
#JsonProperty("custom_content")
private Map<String, Object> customContent;
public IosApsMessage getAps() {
return aps;
}
public void setAps(IosApsMessage aps) {
this.aps = aps;
}
public Map<String, Object> getCustomContent() {
return customContent;
}
public void setCustomContent(Map<String, Object> customContent) {
this.customContent = customContent;
}
}
My serialize result json
{
"aps": {
"alert": "alert",
"sound": "sound",
"badge": 1
},
"custom_content": {
"category": "freetrail",
"type": "state-change",
"status": "rescheduled"
}
}
What I want :
{
"aps": {
"alert": "alert",
"sound": "sound",
"badge": 1
},
"category": "freetrail",
"type": "state-change",
"status": "rescheduled"
}
I don't want the custom_content to be displayed, but I want attribute in custom_content. how can I solve this problem?
I was able to achieve the requested output through a custom serializer. The details will follow. This solution has pros and cons:
Pros
It gets the required output
It allows total freedom in how you generate the output
Cons
It seems that implementing custom serilalizer overrides all other metadata of the target class. i.e. the custom serilalizer ignores annotations of IosNotificationMessage (for example, property names). so you have to supply all the info in the code.
Here is the custom serilalizer:
public static class IosNotificationMessageSerializer extends JsonSerializer<IosNotificationMessage>
{
#Override
public void serialize(IosNotificationMessage msg, JsonGenerator gen, SerializerProvider serializers)
throws IOException, JsonProcessingException
{
gen.writeStartObject();
// serialize IosApsMessage
gen.writeObjectField("aps", msg.getAps());
// serialize map entries sequentially, thus skipping map name
for (Map.Entry<String, Object> customContentEntry : msg.getCustomContent().entrySet()) {
gen.writeObjectField(customContentEntry.getKey(), customContentEntry.getValue());
}
gen.writeEndObject();
}
}
Here is the annotation that associates the custom serilalizer to the application class:
#JsonSerialize(using = IosNotificationMessageSerializer.class)
public class IosNotificationMessage {
...
Calling new ObjectMapper().writeValue(... in the usual manner produces:
{"aps":{"alert":"Message From Baidu Cloud Push-Service","sound":"","badge":0},"key1":"value1","key2":"value2"}

How to serialize type property for object value in map?

I need some help. I must get next json:
{
"433434" : {
"type" : "MULTIPLE",
"value" : [ {
"type" : "NUMBER",
"value" : 322332
}, {
"type" : "NUMBER",
"value" : 322332
} ]
}
}
But I have this:
{
"433434" : {
"value" : [ {
"type" : "NUMBER",
"value" : 322332
}, {
"type" : "NUMBER",
"value" : 322332
} ]
}
}
I am using Jackson. Its my main class
package com.un1acker;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.un1acker.characteristic.AbstractCharacteristic;
import com.un1acker.characteristic.MultipleCharacteristic;
import com.un1acker.characteristic.NumCharacteristic;
import java.io.IOException;
import java.io.StringWriter;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Main {
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
NumCharacteristic numCharacteristic = new NumCharacteristic();
numCharacteristic.setValue(BigInteger.valueOf(322332L));
List<AbstractCharacteristic<?>> list = new ArrayList<>();
list.add(numCharacteristic);
list.add(numCharacteristic);
StringWriter sw = new StringWriter();
MultipleCharacteristic multipleCharacteristic = new MultipleCharacteristic();
multipleCharacteristic.setValue(list);
Map<String, AbstractCharacteristic<?>> map = new HashMap<>();
map.put("433434", multipleCharacteristic);
mapper.writeValue(sw, map);
System.out.println(sw.toString());
}
}
And I have class MultipleCharateristic and NumberCharacteristic who extends from AbstractCharacteristic. Planned that we have map>, that contains in value MultipleCharacteristic.
In the MultipleCharacteristic set list of NumberCharacteristic values.
package com.un1acker.characteristic;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type"
)
#JsonSubTypes({ #JsonSubTypes.Type(
value = NumCharacteristic.class,
name = "NUMBER"
), #JsonSubTypes.Type(
value = MultipleCharacteristic.class,
name = "MULTIPLE")})
public abstract class AbstractCharacteristic<T>{
private static final long serialVersionUID = -6524899961842198462L;
private T value;
public AbstractCharacteristic() {
}
public T getValue() {
return this.value;
}
protected void setValue(T value) {
this.value = value;
}
}
NumCharacteristic Class
package com.un1acker.characteristic;
import java.math.BigInteger;
public class NumCharacteristic extends AbstractCharacteristic<BigInteger> {
private static final long serialVersionUID = 9220460768952701281L;
public NumCharacteristic() {
}
public void setValue(BigInteger value) {
super.setValue(value);
}
}
MultipleCharacteristic class
package com.un1acker.characteristic;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.un1acker.MyCustomSerializer;
import java.util.List;
public class MultipleCharacteristic extends AbstractCharacteristic<List<? extends AbstractCharacteristic<?>>> {
#Override
public void setValue(List<? extends AbstractCharacteristic<?>> value) {
super.setValue(value);
}
#Override
#JsonSerialize(using = MyCustomSerializer.class)
public List<? extends AbstractCharacteristic<?>> getValue() {
return super.getValue();
}
}
I tried create custom serialize for the MultipleClass with override method serializeWithType but this doesnt work.
This looks like it might be a Jackson bug. Your MultipleCharacteristic type serializes fine and includes the type when on its own, but not when it's a map value, which I would expect to work the same way:
#Test // passes
public void serialize_multiple_characteristic() throws Exception {
MultipleCharacteristic chr = new MultipleCharacteristic();
chr.setValue(new ArrayList<>());
ObjectMapper mapper = new ObjectMapper();
assertThat(mapper.writeValueAsString(chr), equivalentTo("{ type: 'MULTIPLE', value: [] }"));
}
#Test // fails, got: {"xyzzy":{"value":[]}}
public void serialize_multiple_characteristic_in_map_value() throws Exception {
MultipleCharacteristic chr = new MultipleCharacteristic();
chr.setValue(new ArrayList<>());
ObjectMapper mapper = new ObjectMapper();
Map<String, MultipleCharacteristic> map = new HashMap<>();
map.put("xyzzy", chr);
assertThat(mapper.writeValueAsString(map), equivalentTo("{ 'xyzzy': { type: 'MULTIPLE', value: [] } }"));
}
(looking through the innards, the BeanSerializer never gets wrapped in TypeWrappedSerializer)
This might not be a problem for you if the map is embedded in something as, as if Jackson knows the type parameters for the map (which it would normally get from a containing bean, for example) it seems to do the right thing:
#Test // passes
public void serialize_multiple_characteristic_in_map_value_using_writer() throws Exception {
MultipleCharacteristic chr = new MultipleCharacteristic();
chr.setValue(new ArrayList<>());
ObjectMapper mapper = new ObjectMapper();
Map<String, AbstractCharacteristic<?>> map = new HashMap<>();
map.put("xyzzy", chr);
// Hint to Jackson what types will be in the map
TypeReference<?> mapType = new TypeReference<Map<String, AbstractCharacteristic<?>>>(){};
assertThat(mapper.writerFor(mapType).writeValueAsString(map), equivalentTo("{ 'xyzzy': { type: 'MULTIPLE', value: [] } }"));
}
One workaround, if you just need to get the JSON produced and can't change the code to use ObjectWrite, is to use ObjectNode instead of Map:
#Test
public void serialize_multiple_characteristic_in_json_node() throws Exception {
MultipleCharacteristic chr = new MultipleCharacteristic();
chr.setValue(new ArrayList<>());
ObjectMapper mapper = new ObjectMapper();
ObjectNode node = mapper.getNodeFactory().objectNode();
node.putPOJO("xyzzy", chr);
assertThat(mapper.writeValueAsString(node), equivalentTo("{ 'xyzzy': { type: 'MULTIPLE', value: [] } }"));
}
This works for me: Add custom serializer for MultipleCharacteristic:
public class MultipleCharacteristicValueSerializer extends JsonSerializer<MultipleCharacteristic> {
#Override
public void serialize(MultipleCharacteristic multipleCharacteristicValue, JsonGenerator jsonGenerator,SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
jsonGenerator.writeFieldName("value");
jsonGenerator.writeStartArray();
for (AbstractCharacteristic<?> characteristicValue : multipleCharacteristicValue.getValue()) {
jsonGenerator.writeObject(characteristicValue);
}
jsonGenerator.writeEndArray();
}
#Override
public void serializeWithType(MultipleCharacteristic value, JsonGenerator jgen, SerializerProvider provider, TypeSerializer typeSer) throws IOException, JsonProcessingException {
typeSer.writeTypePrefixForObject(value, jgen);
serialize(value, jgen, provider);
typeSer.writeTypeSuffixForObject(value, jgen);
}
#Override
public Class<MultipleCharacteristic> handledType() {
return MultipleCharacteristic.class;
}
}

Jackson deserialization of type with different objects

I have a result from a web service that returns either a boolean value or a singleton map, e.g.
Boolean result:
{
id: 24428,
rated: false
}
Map result:
{
id: 78,
rated: {
value: 10
}
}
Individually I can map both of these easily, but how do I do it generically?
Basically I want to map it to a class like:
public class Rating {
private int id;
private int rated;
...
public void setRated(?) {
// if value == false, set rated = -1;
// else decode "value" as rated
}
}
All of the polymorphic examples use #JsonTypeInfo to map based on a property in the data, but I don't have that option in this case.
EDIT
The updated section of code:
#JsonProperty("rated")
public void setRating(JsonNode ratedNode) {
JsonNode valueNode = ratedNode.get("value");
// if the node doesn't exist then it's the boolean value
if (valueNode == null) {
// Use a default value
this.rating = -1;
} else {
// Convert the value to an integer
this.rating = valueNode.asInt();
}
}
No no no. You do NOT have to write a custom deserializer. Just use "untyped" mapping first:
public class Response {
public long id;
public Object rated;
}
// OR
public class Response {
public long id;
public JsonNode rated;
}
Response r = mapper.readValue(source, Response.class);
which gives value of Boolean or java.util.Map for "rated" (with first approach); or a JsonNode in second case.
From that, you can either access data as is, or, perhaps more interestingly, convert to actual value:
if (r.rated instanceof Boolean) {
// handle that
} else {
ActualRated actual = mapper.convertValue(r.rated, ActualRated.class);
}
// or, if you used JsonNode, use "mapper.treeToValue(ActualRated.class)
There are other kinds of approaches too -- using creator "ActualRated(boolean)", to let instance constructed either from POJO, or from scalar. But I think above should work.
You have to write your own deserializer. It could look like this:
#SuppressWarnings("unchecked")
class RatingJsonDeserializer extends JsonDeserializer<Rating> {
#Override
public Rating deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Map<String, Object> map = jp.readValueAs(Map.class);
Rating rating = new Rating();
rating.setId(getInt(map, "id"));
rating.setRated(getRated(map));
return rating;
}
private int getInt(Map<String, Object> map, String propertyName) {
Object object = map.get(propertyName);
if (object instanceof Number) {
return ((Number) object).intValue();
}
return 0;
}
private int getRated(Map<String, Object> map) {
Object object = map.get("rated");
if (object instanceof Boolean) {
if (((Boolean) object).booleanValue()) {
return 0; // or throw exception
}
return -1;
}
if (object instanceof Map) {
return getInt(((Map<String, Object>) object), "value");
}
return 0;
}
}
Now you have to tell Jackson to use this deserializer for Rating class:
#JsonDeserialize(using = RatingJsonDeserializer.class)
class Rating {
...
}
Simple usage:
ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.readValue(json, Rating.class));
Above program prints:
Rating [id=78, rated=10]
for JSON:
{
"id": 78,
"rated": {
"value": 10
}
}
and prints:
Rating [id=78, rated=-1]
for JSON:
{
"id": 78,
"rated": false
}
I found a nice article on the subject: http://programmerbruce.blogspot.com/2011/05/deserialize-json-with-jackson-into.html
I think that the approach of parsing into object, is possibly problematic, because when you send it, you send a string. I am not sure it is an actual issue, but it sounds like some possible unexpected behavior.
example 5 and 6 show that you can use inheritance for this.
Example:
Example 6: Simple Deserialization Without Type Element To Container Object With Polymorphic Collection
Some real-world JSON APIs have polymorphic type members, but don't include type elements (unlike the JSON in the previous examples). Deserializing such sources into polymorphic collections is a bit more involved. Following is one relatively simple solution. (This example includes subsequent serialization of the deserialized Java structure back to input JSON, but the serialization is relatively uninteresting.)
// input and output:
// {
// "animals":
// [
// {"name":"Spike","breed":"mutt","leash_color":"red"},
// {"name":"Fluffy","favorite_toy":"spider ring"},
// {"name":"Baldy","wing_span":"6 feet",
// "preferred_food":"wild salmon"}
// ]
// }
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.Version;
import org.codehaus.jackson.map.DeserializationContext;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.deser.StdDeserializer;
import org.codehaus.jackson.map.module.SimpleModule;
import org.codehaus.jackson.node.ObjectNode;
import fubar.CamelCaseNamingStrategy;
public class Foo
{
public static void main(String[] args) throws Exception
{
AnimalDeserializer deserializer =
new AnimalDeserializer();
deserializer.registerAnimal("leash_color", Dog.class);
deserializer.registerAnimal("favorite_toy", Cat.class);
deserializer.registerAnimal("wing_span", Bird.class);
SimpleModule module =
new SimpleModule("PolymorphicAnimalDeserializerModule",
new Version(1, 0, 0, null));
module.addDeserializer(Animal.class, deserializer);
ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(
new CamelCaseNamingStrategy());
mapper.registerModule(module);
Zoo zoo =
mapper.readValue(new File("input_6.json"), Zoo.class);
System.out.println(mapper.writeValueAsString(zoo));
}
}
class AnimalDeserializer extends StdDeserializer<Animal>
{
private Map<String, Class<? extends Animal>> registry =
new HashMap<String, Class<? extends Animal>>();
AnimalDeserializer()
{
super(Animal.class);
}
void registerAnimal(String uniqueAttribute,
Class<? extends Animal> animalClass)
{
registry.put(uniqueAttribute, animalClass);
}
#Override
public Animal deserialize(
JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException
{
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
ObjectNode root = (ObjectNode) mapper.readTree(jp);
Class<? extends Animal> animalClass = null;
Iterator<Entry<String, JsonNode>> elementsIterator =
root.getFields();
while (elementsIterator.hasNext())
{
Entry<String, JsonNode> element=elementsIterator.next();
String name = element.getKey();
if (registry.containsKey(name))
{
animalClass = registry.get(name);
break;
}
}
if (animalClass == null) return null;
return mapper.readValue(root, animalClass);
}
}
class Zoo
{
public Collection<Animal> animals;
}
abstract class Animal
{
public String name;
}
class Dog extends Animal
{
public String breed;
public String leashColor;
}
class Cat extends Animal
{
public String favoriteToy;
}
class Bird extends Animal
{
public String wingSpan;
public String preferredFood;
}
I asked a similar question - JSON POJO consumer of polymorphic objects
You have to write your own deserialiser that gets a look-in during the deserialise process and decides what to do depending on the data.
There may be other easier methods but this method worked well for me.

Categories

Resources