Flexjson - how to serialize a complex hierarchy including a Map - java

Using Flexjson, I am trying to serialize an object ("Payload") that contains a List. Each MyBean has a field "items", which is a Map>. When I serialize this Payload object, the map field ("items") is empty.
public class Payload {
private List<MyBean> myBeans = new ArrayList<MyBean>();
//the JSON returned has blank values for myBeans.values.items
public String toJson() {
return new JSONSerializer()
.exclude("*.class")
.include("myBeans")
.serialize(this);
}
}
However, when I serialize the MyBean object directly, it works fine.
public class MyBean {
private Map<String, List<SomeBean>> items = new HashMap<String, List<SomeBean>>();
//this works
public String toJson() {
return new JSONSerializer()
.exclude("*.class")
.deepSerialize(this);
}
}
Any suggestions?

After trying a bunch of things, I found this solution.
I created a custom transformer for maps. Just copied the Flexjson MapTransformer and commented out a IF condition. New code below
public class Payload {
private List<MyBean> myBeans = new ArrayList<MyBean>();
//the JSON returned has blank values for myBeans.values.items
public String toJson() {
return new JSONSerializer()
.exclude("*.class")
.include("myBeans")
.transform(new SOMapTransformer(), Map.class)
.serialize(this);
}
}
public class MyBean {
private Map<String, List<SomeBean>> items = new HashMap<String, List<SomeBean>>();
//this works
public String toJson() {
return new JSONSerializer()
.exclude("*.class")
.transform(new SOMapTransformer(), "items")
.deepSerialize(this);
}
}
Here is the custom SOMapTransformer:
import com.mycompany.mypackage.SomeBean;
import flexjson.JSONContext;
import flexjson.Path;
import flexjson.TypeContext;
import flexjson.transformer.AbstractTransformer;
import flexjson.transformer.TransformerWrapper;
public class SOMapTransformer extends AbstractTransformer {
public void transform(Object object) {
JSONContext context = getContext();
Path path = context.getPath();
Map<String, List<SomeBean>> value = (Map<String, List<SomeBean>>) object;
TypeContext typeContext = getContext().writeOpenObject();
for (Object key : value.keySet()) {
path.enqueue((String) key);
//DPD 2013-11-04: This bloody line of code cost me 12 hours. Comment it out!
// if (context.isIncluded((String) key, value.get(key))) {
TransformerWrapper transformer = (TransformerWrapper)context.getTransformer(value.get(key));
if(!transformer.isInline()) {
if (!typeContext.isFirst()) getContext().writeComma();
typeContext.setFirst(false);
getContext().writeName(key.toString());
}
typeContext.setPropertyName(key.toString());
transformer.transform(value.get(key));
// }
path.pop();
}
getContext().writeCloseObject();
}

Related

How to read a com.fasterxml.jackson.databind.node.TextNode from a Mongo DB and convert to a Map <String, Object>?

We are using SpringDataMongoDB in a Spring-boot app to manage our data.
Our previous model was this:
public class Response implements Serializable {
//...
private JsonNode errorBody; //<-- Dynamic
//...
}
JsonNode FQDN is com.fasterxml.jackson.databind.JsonNode
Which saved documents like so in the DB:
"response": {
...
"errorBody": {
"_children": {
"code": {
"_value": "Error-code-value",
"_class": "com.fasterxml.jackson.databind.node.TextNode"
},
"message": {
"_value": "Error message value",
"_class": "com.fasterxml.jackson.databind.node.TextNode"
},
"description": {
"_value": "Error description value",
"_class": "com.fasterxml.jackson.databind.node.TextNode"
}
},
"_nodeFactory": {
"_cfgBigDecimalExact": false
},
"_class": "com.fasterxml.jackson.databind.node.ObjectNode"
},
...
}
We've saved hundreds of documents like this on the production database without ever the need to read them programmatically as they are just kind of logs.
As we noticed that this output could be difficult to read in the future, we've decided to change the model to this:
public class Response implements Serializable {
//...
private Map<String,Object> errorBody;
//...
}
The data are now saved like so:
"response": {
...
"errorBody": {
"code": "Error code value",
"message": "Error message value",
"description": "Error description value",
...
},
...
}
Which, as you may have noticed is pretty much more simple.
When reading the data, ex: repository.findAll()
The new format is read without any issue.
But we face these issues with the old format:
org.springframework.data.mapping.MappingException: No property v found on entity class com.fasterxml.jackson.databind.node.TextNode to bind constructor parameter to!
Or
org.springframework.data.mapping.model.MappingInstantiationException: Failed to instantiate com.fasterxml.jackson.databind.node.ObjectNode using constructor NO_CONSTRUCTOR with arguments
Of course the TextNode class has a constructor with v as param but the property name is _value and ObjectNode has no default constructor: We simply can't change that.
We've created custom converters that we've added to our configurations.
public class ObjectNodeWriteConverter implements Converter<ObjectNode, DBObject> {
#Override
public DBObject convert(ObjectNode source) {
return BasicDBObject.parse(source.toString());
}
}
public class ObjectNodeReadConverter implements Converter<DBObject, ObjectNode> {
#Override
public ObjectNode convert(DBObject source) {
try {
return new ObjectMapper().readValue(source.toString(), ObjectNode.class);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
We did the same for TextNode
But we still got the errors.
The converters are read as we have a ZonedDateTimeConverter that is doing his job.
We can not just wipe out or ignore the old data as we need to read them too in order to study them.
How can we set up a custom reader that will not fail reading the old format ?
As I understood your issue, with the first model, you didn't really have a problem to save or to read in database but, once you wanted to fetch these datas, you noticed that the output is difficult to read. So your problem is to fetch a well readable output then you don't need to change the first model but to extends these classes and overide the toString method to change its behavior while fetching.
There are at least three classes to extends:
TextNode : you can't overide the toString method do that the custom class just print the value
ObjectNode : I can see that there are at least four field inside this class that you want to fecth the value: code, message, description. They are type of TextNode so you can replace them by thier extended classes. Then overide the toString method so that It print fieldName: field.toString() for each field
JsonNode : You can then extend this class and use the custom classes created above, overide the toString method so that It print as you want and use It instead of the common JsonNode
To work like that will make you avoid the way you save or you read the datas but just to fecth on the view.
You can consider it as a little part of the SOLID principle especially the OCP (Open an close principle: avoid to change the class behavoir but extends it to create a custom behavior) and the LSP (Liskov Substitution Principle: Subtypes must be behaviorlly substituable for thier base types).
Since old format is predefined and you know a structure of it you can implement custom deserialiser to handle old and new format at the same time. If errorBody JSON Object contains any of these keys: _children, _nodeFactory or _class you know it is an old format and you need to iterate over keys in _children JSON Object and get _value key to find a real value. Rest of keys and values you can ignore. Simple implementation could look like below:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Data;
import lombok.ToString;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class JsonMongo2FormatsApp {
public static void main(String[] args) throws IOException {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
JsonMapper mapper = JsonMapper.builder().build();
Response response = mapper.readValue(jsonFile, Response.class);
System.out.println(response.getErrorBody());
}
}
#Data
#ToString
class Response {
#JsonDeserialize(using = ErrorMapJsonDeserializer.class)
private Map<String, String> errorBody;
}
class ErrorMapJsonDeserializer extends JsonDeserializer<Map<String, String>> {
#Override
public Map<String, String> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
TreeNode root = p.readValueAsTree();
if (!root.isObject()) {
// ignore everything except JSON Object
return Collections.emptyMap();
}
ObjectNode objectNode = (ObjectNode) root;
if (isOldFormat(objectNode)) {
return deserialize(objectNode);
}
return toMap(objectNode);
}
protected boolean isOldFormat(ObjectNode objectNode) {
final List<String> oldFormatKeys = Arrays.asList("_children", "_nodeFactory", "_class");
final Iterator<String> iterator = objectNode.fieldNames();
while (iterator.hasNext()) {
String field = iterator.next();
return oldFormatKeys.contains(field);
}
return false;
}
protected Map<String, String> deserialize(ObjectNode root) {
JsonNode children = root.get("_children");
Map<String, String> result = new LinkedHashMap<>();
children.fields().forEachRemaining(entry -> {
result.put(entry.getKey(), entry.getValue().get("_value").toString());
});
return result;
}
private Map<String, String> toMap(ObjectNode objectNode) {
Map<String, String> result = new LinkedHashMap<>();
objectNode.fields().forEachRemaining(entry -> {
result.put(entry.getKey(), entry.getValue().toString());
});
return result;
}
}
Above deserialiser should handle both formats.
Michal Ziober's answer did not completely solve the problem as we need to tell SpringData MongoDb that we want it to use the custom deserializer
(Annotating the model does not work with Spring data mongodb):
Define the custom deserializer
public class ErrorMapJsonDeserializer extends JsonDeserializer<Map<String, Object>> {
#Override
public Map<String, Object> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
TreeNode root = p.readValueAsTree();
if (!root.isObject()) {
// ignore everything except JSON Object
return Collections.emptyMap();
}
ObjectNode objectNode = (ObjectNode) root;
if (isOldFormat(objectNode)) {
return deserialize(objectNode);
}
return toMap(objectNode);
}
protected boolean isOldFormat(ObjectNode objectNode) {
final List<String> oldFormatKeys = Arrays.asList("_children", "_nodeFactory", "_class");
final Iterator<String> iterator = objectNode.fieldNames();
while (iterator.hasNext()) {
String field = iterator.next();
return oldFormatKeys.contains(field);
}
return false;
}
protected Map<String, Object> deserialize(ObjectNode root) {
JsonNode children = root.get("_children");
if (children.isArray()) {
children = children.get(0);
children = children.get("_children");
}
return extractValues(children);
}
private Map<String, Object> extractValues(JsonNode children) {
Map<String, Object> result = new LinkedHashMap<>();
children.fields().forEachRemaining(entry -> {
String key = entry.getKey();
if (!key.equals("_class"))
result.put(key, entry.getValue().get("_value").toString());
});
return result;
}
private Map<String, Object> toMap(ObjectNode objectNode) {
Map<String, Object> result = new LinkedHashMap<>();
objectNode.fields().forEachRemaining(entry -> {
result.put(entry.getKey(), entry.getValue().toString());
});
return result;
}
}
Create a Custom mongo converter and pass it the custom deserializer.
Actually we do not pass the serializer directly but by means of an ObjectMapper configured with that Custom deserializer
public class CustomMappingMongoConverter extends MappingMongoConverter {
//The configured objectMapper that will be passed during instantiation
private ObjectMapper objectMapper;
public CustomMappingMongoConverter(DbRefResolver dbRefResolver, MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext, ObjectMapper objectMapper) {
super(dbRefResolver, mappingContext);
this.objectMapper = objectMapper;
}
#Override
public <S> S read(Class<S> clazz, Bson dbObject) {
try {
return objectMapper.readValue(dbObject.toString(), clazz);
} catch (IOException e) {
throw new RuntimeException(dbObject.toString(), e);
}
}
//in case you want to serialize with your custom objectMapper as well
#Override
public void write(Object obj, Bson dbo) {
String string = null;
try {
string = objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new RuntimeException(string, e);
}
((DBObject) dbo).putAll((DBObject) BasicDBObject.parse(string));
}
}
Create and configure the object mapper then instantiate the custom MongoMappingConverter and add it to Mongo configurations
public class MongoConfiguration extends AbstractMongoClientConfiguration {
//... other configuration method beans
#Bean
#Override
public MappingMongoConverter mappingMongoConverter() throws Exception {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory());
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.registerModule(new SimpleModule() {
{
addDeserializer(Map.class, new ErrorMapJsonDeserializer());
}
});
return new CustomMappingMongoConverter(dbRefResolver, mongoMappingContext(), objectMapper);
}
}

Convert ArrayList of custom objects to JSON

I have an ArrayList of custom Objects. Each of these objects have an arraylist of another custom object. Then these second level of custom objects have an arraylist of another custom object.
This is how the class hierarchy looks like
public class Undle {
private String undleStatus;
private ArrayList<ArcelFolder> arcelFolders;
public ArrayList<ArcelFolder> getArcelFolders() {
return arcelFolders;
}
public void setArcelFolders(ArrayList<ArcelFolder> arcelFolders) {
this.arcelFolders = arcelFolders;
}
//Other getter and setters
}
public class ArcelFolder {
private ArrayList<ArcelDocument> arcelDocuments;
private String arcelStatus;
public String getArcelStatus() {
return arcelStatus;
}
public void setArcelStatus(String arcelStatus) {
this.arcelStatus = arcelStatus;
}
public ArrayList<ArcelDocument> getArcelDocuments() {
return arcelDocuments;
}
public void setArcelDocuments(ArrayList<ArcelDocument> arcelDocuments) {
this.arcelDocuments = arcelDocuments;
}
}
public class ArcelDocument {
private String gain;
public String getGain() {
return gain;
}
public void setGain(String gain) {
this.gain = gain;
}
}
I have an arraylist of Undle objects
ArrayList<Undle> undleList = new ArrayList<Undle>();
// Create objects of ArcelFolder and ArcelDocument
// Add ArcelDocument list to ArcelFolder
// Add ArcelFolder list to Undle arraylist
I would like to convert Undle ArrayList to a JSON. How can I flatten this hierarcical structure of beans and put it in a JSON?
I tried doing something like
org.json.simple.JSONObject resultObj = new JSONObject(undleList);
and
org.json.simple.JSONArray arr = new JSONArray(undleList);
But it seems that they work only if a String ArrayList is passed.
Gson gson = new Gson();
Type type = new TypeToken<List<Bundle>>() {}.getType();
String json = gson.toJson(bundleList, type);
System.out.println(json);
List<Bundle> fromJson = gson.fromJson(json, type);
for (Bundle bundle : fromJson) {
System.out.println(bundle);
}

How to only store non-default values with Gson

I made a litte serialization system, that uses Gson and only affects Fields with a specific Annotation.
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
public #interface Store {
String valueName() default "";
boolean throwIfDefault() default false;
}
throwIfDefault() determines whether or not the Field should be saved to the file if it's value equals to the default value (I check that by comparing the field's value to the same field but in a static instance of the class).
It works perfectly, but what I'm trying to achive, is that the same works for the Map, Array and Set objects:
The entries of these objects should only be saved, if they are not contained in the default instatiation of that particular Field.
It also has to work for deserialisation:
The default values that are not yet in the loaded object, should be added during deserialisation or the default object is loaded first and then modified with the entries of the loaded object.
Is this possible by creating a custom Json(De)Serializer for these obejcts or how would you do it?
Here's the de-serialization part:
public void Load() throws FileNotFoundException {
Type typeOfHashMap = new TypeToken<LinkedHashMap<String, Object>>(){}.getType();
FileReader reader = new FileReader(file);
HashMap<String, Object> loadedMap = mainGson.fromJson(reader,typeOfHashMap);
for(Field f: this.getClass().getDeclaredFields()) {
if (!f.isAnnotationPresent(Store.class)) {
continue;
}
try {
f.setAccessible(true);
Store annotation = f.getAnnotation(Store.class);
Object defaultValue = DefaultRegistrator.getDefault(this.getClass(),f);
if (!loadedMap.containsKey(annotation.valueName())) {
f.set(this, defaultValue);
continue;
}
Object loadedValue = mainGson.fromJson(
loadedMap.get(annotation.valueName()).toString(), f.getType()
);
f.set(this, loadedValue);
} catch(IllegalAccessException e) {
}
}
}
Let's say your JSON object is
{"defParam1": 999,
"defParam2": 999,
"defParam3": 999,
"param4": 999,
"param5": 999,
"param6": 999}
The parameter defParam1, defParam2, defParam3 not will be set.
Parsing JSON object to specific object with default parameters
The default parameters are set in the constructor, so you don't need using annotation
Your Java Object is:
public class ObjStore {
public ObjStore(){
this(false);
}
// Load default parameters directly into the constructor
public ObjStore(boolean loadDefault){
if( loadDefault ){
defParam1 = 123; // (int) DefaultRegistrator.getDefault("ObjStore", "defParam1");
defParam2 = 123; // (int) DefaultRegistrator.getDefault("ObjStore", "defParam2");
defParam3 = 123; // (int) DefaultRegistrator.getDefault("ObjStore", "defParam3");
}
}
public int getDefParam1() {
return defParam1;
}
public int getDefParam2() {
return defParam2;
}
public int getDefParam3() {
return defParam3;
}
public int getParam4() {
return param4;
}
public void setParam4(int param4) {
this.param4 = param4;
}
public int getParam5() {
return param5;
}
public void setParam5(int param5) {
this.param5 = param5;
}
public int getParam6() {
return param6;
}
public void setParam6(int param6) {
this.param6 = param6;
}
private int defParam1;
private int defParam2;
private int defParam3;
private int param4;
private int param5;
private int param6;
}
For deserialization you need to register new custom typeAdapter in this way:
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(ObjStore.class, new JsonDeserializer<ObjStore>() {
public ObjStore deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
ObjStore objStore = new ObjStore(true);
JsonObject jo = json.getAsJsonObject();
objStore.setParam4( jo.get("param4").getAsInt() );
objStore.setParam5(jo.get("param5").getAsInt());
objStore.setParam6(jo.get("param6").getAsInt());
return objStore;
}
});
Gson gson = gsonBuilder.create();
You parse the JSON object using:
ObjStore objStore = gson.fromJson("{\"defParam1\": 999,\"defParam2\": 999,\"defParam3\": 999,\"param4\": 999,\"param5\": 999,\"param6\": 999}", ObjStore.class);
Parsing JSON object to Map object with default parameters
The default parameters are set in the constructor, so you don't need using annotation.
Define this class that wrap your Map object
public class ObjMapStore {
public ObjMapStore(){
this(true);
}
public ObjMapStore(boolean loadDefault){
map = new HashMap<>();
if(loadDefault){
map.put("defParam1", 123); // (int) DefaultRegistrator.getDefault("ObjMapStore", "defParam1");
map.put("defParam2", 123); // (int) DefaultRegistrator.getDefault("ObjMapStore", "defParam2");
map.put("defParam3", 123); // (int) DefaultRegistrator.getDefault("ObjMapStore", "defParam3");
}
}
public void put(String key, Object value){
map.put(key, value);
}
public Map<String, Object> getMap(){
return map;
}
private Map<String, Object> map;
}
Again for deserialization you need to register new custom typeAdapter in this way:
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(ObjMapStore.class, new JsonDeserializer<ObjMapStore>() {
public ObjMapStore deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
ObjMapStore objMapStore = new ObjMapStore();
JsonObject jo = json.getAsJsonObject();
objMapStore.put("param4", jo.get("param4").getAsInt());
objMapStore.put("param5", jo.get("param5").getAsInt());
objMapStore.put("param6", jo.get("param6").getAsInt());
return objMapStore;
}
});
Gson gson = gsonBuilder.create();
As done before you get the map Object using this:
Map<String, Object> objMapStore = gson.fromJson("{\"defParam1\": 999,\"defParam2\": 999,\"defParam3\": 999,\"param4\": 999,\"param5\": 999,\"param6\": 999}", ObjMapStore.class).getMap();
Stay alert to this call .getMap(); because allow you to get the Map defined into the object returned by ObjMapStore
Glad to have helped, Write a comment for any question. Remember to vote up and check the response if it helped. Byee

Deserialize a JSON response with Gson containing a field of variable type

The responses of a REST API always return a JSON with the following structure:
{
"status": "<status_code>",
"data": <data_object>
}
My problem is that the value of data doesn't have an unique type, but it can be a String, a JSON Object or a JSON Array, depending on the called endpoint. I can't figure out how to deserialize it in the right way to create the different Java objects...
For example, I've already prepared some POJOs: the root element
public class ApiResult {
#SerializedName("status")
public String status;
#SerializedName("data")
public JsonElement data; // should I define it as a JsonElement??
}
and two objects that reflects two of the endpoints:
// "data" can be a list of NavItems
public class NavItem {
#SerializedName("id")
public String id;
#SerializedName("name")
public String name;
#SerializedName("icon")
public String icon;
#SuppressWarnings("serial")
public static class List extends ArrayList<NavItem> {}
}
and
// "data" can be a single object representing a Profile
public class Profile {
#SerializedName("id")
public String id;
#SerializedName("fullname")
public String fullname;
#SerializedName("avatar")
public String avatar;
}
Reading some StackOverflow questions, I've seen I should use the JsonDeserializer<T> interface. But how if the type of data in ApiResult is variable?
You should use a a custom JsonDeserializer and write all your logic there, like this
ApiResult.java
public class ApiResult {
#SerializedName("status")
public String status;
#SerializedName("data")
public Object data;
}
ApiResultDeserializer.java
import java.lang.reflect.Type;
import java.util.List;
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.reflect.TypeToken;
public class ApiResultDeserializer implements JsonDeserializer<ApiResult> {
private Type listType = new TypeToken<List<NavItem>>(){}.getType();
#Override
public ApiResult deserialize(JsonElement value, Type type,
JsonDeserializationContext context) throws JsonParseException {
final JsonObject apiResultJson = value.getAsJsonObject();
final ApiResult result = new ApiResult();
result.status = apiResultJson.get("status").getAsString();
JsonElement dataJson = apiResultJson.get("data");
if(dataJson.isJsonObject()) {
result.data = context.deserialize(dataJson, NavItem.class);
} else if(dataJson.isJsonPrimitive()) {
result.data = context.deserialize(dataJson, String.class);
} else if(dataJson.isJsonArray()) {
result.data = context.deserialize(dataJson, listType);
}
return result;
}
}
and try to create different kinds of data (List, Object, or String) as you mentioned
Main.java
Gson gson = new GsonBuilder()
.registerTypeAdapter(ApiResult.class, new ApiResultDeserializer())
.create();
List<NavItem> navItems = new ArrayList<NavItem>();
for(int i = 1 ; i < 6 ; ++i) {
navItems.add(new NavItem(i+"", "Name-" + i, "Icon-" + i ));
}
ApiResult result = new ApiResult();
result.status = "OK";
result.data = navItems;
// Serialization
System.out.println(gson.toJson(result)); // {\"status\":\"OK\",\"data\":[{\"id\":\"1\",\"name\":\"Name-1\",\"icon\":\"Icon-1\"},{\"id\":\"2\",\"name\":\"Name-2\",\"icon\":\"Icon-2\"},{\"id\":\"3\",\"name\":\"Name-3\",\"icon\":\"Icon-3\"},{\"id\":\"4\",\"name\":\"Name-4\",\"icon\":\"Icon-4\"},{\"id\":\"5\",\"name\":\"Name-5\",\"icon\":\"Icon-5\"}]}
result.data = navItems.get(0);
System.out.println(gson.toJson(result)); // {\"status\":\"OK\",\"data\":{\"id\":\"1\",\"name\":\"Name-1\",\"icon\":\"Icon-1\"}}
result.data = "Test";
System.out.println(gson.toJson(result)); // {\"status\":\"OK\",\"data\":\"Test\"}
// Deserialization
String input = "{\"status\":\"OK\",\"data\":[{\"id\":\"1\",\"name\":\"Name-1\",\"icon\":\"Icon-1\"},{\"id\":\"2\",\"name\":\"Name-2\",\"icon\":\"Icon-2\"},{\"id\":\"3\",\"name\":\"Name-3\",\"icon\":\"Icon-3\"},{\"id\":\"4\",\"name\":\"Name-4\",\"icon\":\"Icon-4\"},{\"id\":\"5\",\"name\":\"Name-5\",\"icon\":\"Icon-5\"}]}";
ApiResult newResult = gson.fromJson(input, ApiResult.class);
System.out.println(newResult.data); // Array
input = "{\"status\":\"OK\",\"data\":{\"id\":\"1\",\"name\":\"Name-1\",\"icon\":\"Icon-1\"}}";
newResult = gson.fromJson(input, ApiResult.class);
System.out.println(newResult.data); // Object
input = "{\"status\":\"OK\",\"data\":\"Test\"}";
newResult = gson.fromJson(input, ApiResult.class);
System.out.println(newResult.data); // String
I managed to make it work as I wanted, and without using any custom deserializer!
For each endpoint, I wait for the response (btw I'm using Volley), then I first generate the "root" ApiResult object, check if the status is OK, then I proceed instantiating the data field as the requested type.
The POJOs are the same of the question. In ApiResult, "data" is a JsonElement.
// ... called the endpoint that returns a NavItem list
public void onResponse(String response) {
ApiResult rootResult = gson.fromJson(response.toString(), ApiResult.class);
if (rootResult.status.equals(STATUS_OK)) {
Log.d(LOG_TAG, response.toString());
NavItem.List resData = gson.fromJson(rootResult.data, NavItem.List.class); // <-- !!!!!
callback.onSuccess(resData);
}
else {
Log.e(LOG_TAG, response.toString());
callback.onError(-1, null);
}
}
Obviously the only thing to change for the "Profile" endpoint is the line with !!!!!

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