I would like to know if it is possible to customize the deserialization of json depending the field name, for example
{
id: "abc123",
field1: {...}
other: {
field1: {....}
}
}
I the previous json, I would like to have a custom deserializer for the fields named "field1", in any level in the json.
The reason: We have our data persisted as JSON, and we have a REST service that returns such data, but before return it, the service must inject extra information in the "field1" attribute.
The types are very dynamic, so we cannot define a Java class to map the json to use annotations.
An first approach was to deserialize to Map.class and then use JsonPath to search the $..field1 pattern, but this process is expensive for bigger objects.
I appreciate any help.
Thanks,
Edwin Miguel
You should consider registering a custom deserializer with the ObjectMapper for this purpose.
Then you should be able to simply map your JSON stream to a Map<String, Object> knowing that your field1 objects will be handled by your custom code.
I create a custom deserializer and added it to a SimpleModule for the ObjectMapper
public class CustomDeserializer extends StdDeserializer<Map> {
public CustomDeserializer() {
super(Map.class);
}
#Override
public Map deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Map<String, Object> result = new HashMap<String, Object>();
jp.nextToken();
while (!JsonToken.END_OBJECT.equals(jp.getCurrentToken())) {
String key = jp.getText();
jp.nextToken();
if ("field1".equals(key)) {
MyObject fiedlObj= jp.readValueAs(MyObject.class);
//inject extra info
//...
result.put(key, fieldObj);
} else {
if (JsonToken.START_OBJECT.equals(jp.getCurrentToken())) {
result.put(key, deserialize(jp, ctxt));
} else if (JsonToken.START_ARRAY.equals(jp.getCurrentToken())) {
jp.nextToken();
List<Object> linkedList = new LinkedList<Object>();
while (!JsonToken.END_ARRAY.equals(jp.getCurrentToken())) {
linkedList.add(deserialize(jp, ctxt));
jp.nextToken();
}
result.put(key, linkedList);
} else {
result.put(key, jp.readValueAs(Object.class));
}
}
jp.nextToken();
}
return result;
}
}
The problem is that I had to implement the parsing for the remaining attributes.
For now, it is my solution...
Related
I simply have a Map. But it can return a Map, which may also return a Map. It's possible up to 3 to 4 nested Maps. So when I want to access a nested value, I need to do this:
((Map)((Map)((Map)CacheMap.get("id")).get("id")).get("id")).get("id")
Is there a cleaner way to do this?
The reason I'm using a Map instead of mapping it to an object is for maintainability (e.g. when there are new fields).
Note:
Map<String, Object>
It has to be Object because it won't always return a Hashmap. It may return a String or a Long.
Further clarification:
What I'm doing is I'm calling an api which returns a json response which I save as a Map.
Here's some helper methods that may help things seem cleaner and more readable:
#SuppressWarnings("unchecked")
public static Map<String, Object> getMap(Map<String, Object> map, String key) {
return (Map<String, Object>)map.get(key);
}
#SuppressWarnings("unchecked")
public static String getString(Map<String, Object> map, String key) {
return (String)map.get(key);
}
#SuppressWarnings("unchecked")
public static Integer geInteger(Map<String, Object> map, String key) {
return (Integer)map.get(key);
}
// you can add more methods for Date, Long, and any other types you know you'll get
But you would have to nest the calls:
String attrValue = getString(getMap(getMap(map, id1), id2), attrName);
Or, if you want something more funky, add the above methods as instance methods to a map impl:
public class FunkyMap extends HashMap<String, Object> {
#SuppressWarnings("unchecked")
public FunkyMap getNode(String key) {
return (FunkyMap)get(key);
}
#SuppressWarnings("unchecked")
public String getString(String key) {
return (String)get(key);
}
#SuppressWarnings("unchecked")
public Integer geInteger(String key) {
return (Integer)get(key);
}
// you can add more methods for Date, Long, and any other types you know you'll get
}
Deserialize into this class with your json library (you'll probably have to provide it with a factory method for the map class impl), then you can chain the calls more naturally:
String attrValue = map.getNode(id1).getNode(id2).getString(attrName);
The funky option is what I did for a company, and it worked a treat :)
If you don't know the depth of the JSON tree and if you worry about maintainability if new fields are added, I would recommend not to deserialize the full tree in a Map but instead use a low-level parser.
For example, if your JSON looks like the following:
{
"id": {
"id": {
"id": {
"id": 22.0
}
}
}
}
You could write something like that to get the id using Jackson:
public Object getId(String json) throws JsonParseException, IOException
{
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(json);
JsonNode id = root.get("id");
while (id != null && id.isObject())
{
id = id.get("id");
}
//Cannot find a JsonNode for the id
if (id == null)
{
return null;
}
//Convert id to either String or Long
if (id.isTextual())
return id.asText();
if (id.isNumber())
return id.asLong();
return null;
}
I have this class whose attributes should be serialized to JSon using Jackson:
public class MicroClinicalInfo {
#JsonProperty
private Map<String, String> clinicalInfo;
#JsonCreator
public MicroClinicalInfo() {}
public MicroClinicalInfo(DtoMicroSampleInfo dto) {
this.clinicalInfo = new HashMap<String, String>();
for(/* each clinical info entity received from EJB in dto */) {
this.clinicalInfo.put(clinicalInfoKey, clinicalInfoDescr);
}
}
}
Now, if clinical info do not exist, produced JSon is {"clinicalInfo":{}} but in this case frontend developer wants me to return an empty array and not an empty object. So, I should return {"clinicalInfo":[]}. How can I do that?
I was trying with:
#JsonGetter
public Map<String, String> getClinicalInfo() {
if (this.clinicalInfo != null && !this.clinicalInfo.isEmpty()) {
return this.clinicalInfo;
}
else {
}
}
but I don't know how to give a uniform vision. Because serialization of a not empty HashMap is correct, but I don't know how to render it as an empty array.
Thank you all
I have an object in which one of the properties is a Map<MyEnum, Object>.
As my application is quite big, I've enabled default typing as so :
ObjectMapper jsonMapper = new ObjectMapper()
.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_OBJECT)
.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
This is rather good, generally speaking.
But, as Javascript doesn't support object keys when using objects as hashes, when I put some data in that map from the javascript side, the object is transformed into a string.
As a consequence, the JSON I receive contains
"MyClass": {
"contextElements": {
"userCredentials": {
"UserCredentials": {
"login": "admin",
"password": "admin",
}
}
}
},
When deserializing that, Jackson fails with the following exception
java.lang.IllegalArgumentException: Invalid type id 'userCredentials' (for id type 'Id.class'): no such class found
at org.codehaus.jackson.map.jsontype.impl.ClassNameIdResolver.typeFromId(ClassNameIdResolver.java:72)
at org.codehaus.jackson.map.jsontype.impl.TypeDeserializerBase._findDeserializer(TypeDeserializerBase.java:61)
at org.codehaus.jackson.map.jsontype.impl.AsWrapperTypeDeserializer._deserialize(AsWrapperTypeDeserializer.java:87)
at org.codehaus.jackson.map.jsontype.impl.AsWrapperTypeDeserializer.deserializeTypedFromObject(AsWrapperTypeDeserializer.java:39)
at org.codehaus.jackson.map.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:133)
at org.codehaus.jackson.map.deser.SettableBeanProperty$MethodProperty.deserializeAndSet(SettableBeanProperty.java:221)
Which I quite well understand : Jackson doesn't understand the Map<MyEnum, Object> declaration in my class and, although MyEnum is a final class, wants some type metadata added (hey, maybe it's a bug ?!).
What can I do to ahve that code working ?
I'm using Jackson 1.5.2
OK, so, the question states it correctly : it is not possible to use JSON maps in which keys are not strings. As a consequence, to emulate the Java Map in javascript, one has to go a longer path, which would typically involve transforming the map into ... something else.
What I chose was the quite usual array of arrays :
a map such as
{
a:b,
c:d,
}
Will then be translated into the array
[
[a,b],
[c,d],
]
What are the detailled steps required to obtain that result
Configure custom (de)serialization
This is obtained by setting a serialization factory into the object mapper, as Jackson doc clearly explains :
/**
* Associates all maps with our custom serialization mechanism, which will transform them into arrays of arrays
* #see MapAsArraySerializer
* #return
*/
#Produces
public SerializerFactory createSerializerFactory() {
CustomSerializerFactory customized = new CustomSerializerFactory();
customized.addGenericMapping(Map.class, new MapAsArraySerializer());
return customized;
}
public #Produces ObjectMapper createMapper() {
ObjectMapper jsonMapper = new ObjectMapper();
// ....
// now configure serializer
jsonMapper.setSerializerFactory(createSerializerFactory());
// ....
return jsonMapper;
}
The process seems quite simple, mainly because serialization provides quite correct polymorphism features in serialization, which are not that good for deserialization. Indeed, as doc also states, deserialization requires adding explicit class mappings, which are not used in any object-oriented fashion (inheritence is not supported there)
/**
* Defines a deserializer for each and any used map class, as there is no inheritence support ind eserialization
* #return
*/
#Produces
public DeserializerProvider createDeserializationProvider() {
// Yeah it's not even a standard Jackson class, it'll be explained why later
CustomDeserializerFactory factory = new MapAsArrayDeserializerFactory();
List<Class<? extends Map>> classesToHandle = new LinkedList<>();
classesToHandle.add(HashMap.class);
classesToHandle.add(LinkedHashMap.class);
classesToHandle.add(TreeMap.class);
for(Class<? extends Map> c : classesToHandle) {
addClassMappingFor(c, c, factory);
}
// and don't forget interfaces !
addClassMappingFor(Map.class, HashMap.class, factory);
addClassMappingFor(SortedMap.class, TreeMap.class, factory);
return new StdDeserializerProvider(factory);
}
private void addClassMappingFor(final Class<? extends Map> detected, final Class<? extends Map> created, CustomDeserializerFactory factory) {
factory.addSpecificMapping(detected, new MapAsArrayDeserializer() {
#Override
protected Map createNewMap() throws Exception {
return created.newInstance();
}
});
}
// It's the same createMapper() method that was described upper
public #Produces ObjectMapper createMapper() {
ObjectMapper jsonMapper = new ObjectMapper();
// ....
// and deserializer
jsonMapper.setDeserializerProvider(createDeserializationProvider());
return jsonMapper;
}
Now we have correctly defined how (de)serialization is customized, or do we have ? In fact, no : the MapAsArrayDeserializerFactory deserves its own explanation.
After some debugging, I found that DeserializerProvider delegates to the DeserializerFactory when no deserializer exists for class, which is cool. But, the DeserializerFactory creates the deserializer according to the "kind" of obejct : if it is a collection, then a CollectionDeserializer will be used (to read the array into a Collection). If it's a Map, then the MapDeserializer will be used.
Unfortunatly, this resolution uses the java class given in the JSON stream (especially when using polymorphic deserialization, which was my case). As a consequence, configuring custom deserialization has no effect, unless the CustomDeserializerFactory is customized ... like that :
public class MapAsArrayDeserializerFactory extends CustomDeserializerFactory {
#Override
public JsonDeserializer<?> createMapDeserializer(DeserializationConfig config, MapType type, DeserializerProvider p) throws JsonMappingException {
return createBeanDeserializer(config, type, p);
}
}
Yup, i deserialize all maps as beans. But now, all my deserializers are correctly called.
Serializing
Now, serialization is a rather simple task :
public class MapAsArraySerializer extends JsonSerializer<Map> {
#SuppressWarnings("unchecked")
private Set asListOfLists(Map<?, ?> value) {
Set returned = new HashSet<>();
for(Map.Entry e : value.entrySet()) {
returned.add(Arrays.asList(e.getKey(), e.getValue()));
}
return returned;
}
#Override
public void serialize(Map value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
Collection entries = asListOfLists(value);
jgen.writeObjectField("entries", entries);
}
#Override
public void serializeWithType(Map value, JsonGenerator jgen, SerializerProvider provider, TypeSerializer typeSer) throws IOException,
JsonProcessingException {
Collection entries = asListOfLists(value);
typeSer.writeTypePrefixForObject(value, jgen);
jgen.writeObjectField("entries", entries);
typeSer.writeTypeSuffixForObject(value, jgen);
}
}
Deserialization
And deserialization is not more complex :
public abstract class MapAsArrayDeserializer<Type extends Map> extends JsonDeserializer<Type> {
protected Type newMap(Collection c, Type returned) {
for(Object o : c) {
if (o instanceof List) {
List l = (List) o;
if(l.size()==2) {
Iterator i = l.iterator();
returned.put(i.next(), i.next());
}
}
}
return returned;
}
protected abstract Type createNewMap() throws Exception;
#Override
public Type deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
if(jp.getCurrentToken().equals(JsonToken.START_OBJECT)) {
JsonToken nameToken = jp.nextToken();
String name = jp.getCurrentName();
if(name.equals("entries")) {
jp.nextToken();
Collection entries = jp.readValueAs(Collection.class);
JsonToken endMap = jp.nextToken();
try {
return newMap(entries, createNewMap());
} catch(Exception e) {
throw new IOException("unable to create receiver map", e);
}
} else {
throw new IOException("expected \"entries\", but field name was \""+name+"\"");
}
} else {
throw new IOException("not startying an object ? Not possible");
}
}
#Override
public Type deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer typeDeserializer) throws IOException,
JsonProcessingException {
Object value = typeDeserializer.deserializeTypedFromObject(jp, ctxt);
return (Type) value;
}
}
Well, expected the class is left abstract t have each declared subtype create the right map instance.
And now
And now it works seamlessly on Java side (cause the Javascript has to have a map-equivalent object to read those datas.
I want to have Jackson always parse numbers as Long or Double.
I have a class like the following with the corresponding getters and setters:
public class Foo {
private HashMap<String, ArrayList<HashMap<String, Object>>> tables;
...
}
And some Json that looks like so:
{ "tables" :
{ "table1" :
[
{ "t1Field1" : 0,
"t1Field2" : "val2"
},
{ "t1Field1" : 1,
"t1Field2" : "val4"
}
]
}
}
Jackson will parse the values for t1Field1 as Integers/Longs and Floats/Doubles based on the size of the number. But I want to always get Longs and Doubles.
I'm almost certain I have to write a custom deserializer or parser to do this and I have looked through examples but haven't found anything that works how I would imagine. I just want to extend existing Jackson functionality and override what happens for numbers. I don't want to write a whole deserializer for Objects. I just want to do something like:
public class CustomerNumberDeserializer extends SomethingFromCoreJackson {
public Object deserialize() {
Object num;
num = super.deserialize();
if (num instanceof Integer)
return Long.valueOf(((Integer)num).intValue());
return num;
}
}
But all the Jackson classes that I thought to extend were either final or abstract and seemed to require a bunch of extra work. Is what I want possible?
After revisiting this I found the class that I wanted to extend. Hope this helps someone.
I created a custom deserializer as follows:
/**
* Custom deserializer for untyped objects that ensures integers are returned as longs
*/
public class ObjectDeserializer extends UntypedObjectDeserializer {
private static final long serialVersionUID = 7764405880012867708L;
#Override
public Object deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
Object out = super.deserialize(jp, ctxt);
if (out instanceof Integer) {
return Long.valueOf((Integer)out).longValue();
}
return out;
}
#Override
public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt,
TypeDeserializer typeDeserializer) throws IOException {
Object out = super.deserializeWithType(jp, ctxt, typeDeserializer);
if (out instanceof Integer) {
return Long.valueOf((Integer)out).longValue();
}
return out;
}
}
And configured my object mapper to use it:
ObjectMapper om = new ObjectMapper();
SimpleModule mod = new SimpleModule().addDeserializer(Object.class, new ObjectDeserializer());
om.registerModule(mod);
to sum it up before the wall of text below :-)
I need help with how to deserialize a Dictionary using Jackson and a custom deserializer.
Right now I have an Android app communication with a .NET (C#) server. They use JSON to communicate.
On the JAVA-side, I am using Jackson to handle the JSON and on the .NET-side I am using the built in DataContractSerializer (I know, ppl will start commenting I should use something else, but Im not so... ;-) )
The problem is that I am sending Dictionaries from C# and I want that to be deserialized to HashMaps om the JAVA-side, but I havent found a good resource for how to do that.
One example of a Dictionary I am sending from C#:
// Here, the object Equipment is the key, and the int following indicates the amount
[DataMember]
public Dictionary<Equipment, int> EquipmentList { get; set; }
And just for reference, the Equipment object in C#:
[DataContract]
public class Equipment
{
[DataMember]
public uint Id { get; set; }
[DataMember]
public string Name { get; set; }
public override bool Equals(object obj)
{
if (obj.GetType() != this.GetType())
return false;
Equipment e = (Equipment)obj;
return e.Id == this.Id;
}
}
Its serialized correctly into decent JSON on the C#-side, the Dictionary looks like this:
//....
"EquipmentList":[
{
"Key":{
"EquipmentId":123,
"Name":"MyName"
},
"Value":1
}
//....
I have added a custom serializer (CustomMapSerializer), like this:
public static ObjectMapper mapper = new ObjectMapper();
private static SimpleDeserializers simpleDeserializers = new SimpleDeserializers();
private static StdDeserializerProvider sp = new StdDeserializerProvider();
public static void InitSerialization()
{
simpleDeserializers.addDeserializer(String.class, new CustomStringDeserializer());
simpleDeserializers.addDeserializer(Map.class, new CustomMapDeserializer());
sp.withAdditionalDeserializers(simpleDeserializers);
mapper.setDeserializerProvider(sp);
}
And decorated the field like this:
#JsonDeserialize(using=CustomMapDeserializer.class)
public Map<Equipment, Integer> EquipmentList;
And finally, when I run it I do get a break in the custom deserializer class, but I am not sure how to proceed from here:
public class CustomMapDeserializer extends JsonDeserializer<Map> {
#Override
public Map deserialize(JsonParser arg0, DeserializationContext arg1) throws IOException, JsonProcessingException
{
return new HashMap<Object, Object>(); // <-- I can break here
}
}
So, what I would like is some input on how to create a HashMap with the correct values in it, ie a deserialized Equipment as Key and an Int as value.
Anyone out there who can assist? =)
Ok, after a while testing and researching, this is what I came up with.
The custom deserializer looks like this:
public class CustomMapCoTravellerDeserializer extends JsonDeserializer<Map> {
#Override
public Map deserialize(JsonParser jp, DeserializationContext arg1) throws IOException, JsonProcessingException
{
HashMap<CoTravellers, Integer> myMap = new HashMap<CoTravellers, Integer>();
CoTravellers ct = new CoTravellers();
jp.nextToken(); // {
jp.nextToken(); // Key
jp.nextToken(); // {
jp.nextToken(); // "CoTravellerId"
jp.nextToken(); // CoTravellerId Id
int coTravellerValue = jp.getIntValue();
jp.nextToken(); // Name
jp.nextToken(); // Name Value
String coTravellerName = jp.getText();
jp.nextToken(); // }
jp.nextToken(); // "Value"
jp.nextToken(); // The value
int nbr = jp.getIntValue();
ct.CoTravellerId = coTravellerValue;
ct.Name = coTravellerName;
myMap.put(ct, nbr);
return myMap;
}
}
I think this will work, if I can only figure out the JsonMappingException I am getting... but I will post on that separately =)