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);
Related
I want to write json deserializer on class Type so that when Type is deserialized from given json based on name it maps value (of type interface Being) to its current implementation based on some factory method that returns correct class name based on name, and populates remaining class without any explicit deserialization and without creating object of TigerBeing or HumanBeing explicitly using new.
I tried to use #jsonCreator but there i have to initialize entire HumanBeing or TigerBeing using new and passing all json in constructor. I need auto mapping for types further used as further pojo can be quite complex.
{type:[{
"name": "Human",
"value": {
"height":6,
"weight":100,
"languages":["spanish","english"]
}
},
{
"name":"Tiger",
"value":{
"extinct":1,
"found":["Asia", "America", "Europe", "Africa"]
}
}
]}
I have:
public class Type {
String name;
Being value;
}
public interface Being {
}
public class TigerBeing implements Being {
Integer extinct;
String[] found;
}
public class HumanBeing implement Being {
Integer height;
Integer weight;
String[] languages;
}
import java.io.IOException;
public class BeingDeserializer extends JsonDeserializer<Being> {
#Override
public Expertise deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonMappingException {
JsonNode node = jp.getCodec().readTree(jp);
String beingName = node.get("name").asText();
JsonNode valueNode = node.get("value");
BeingName beingByName = ExpertiseName.getBeingByName(beingName);
if(beingByName ==null) {
throw new JsonMappingException("Invalid Being " + beingName);
}
Being being = JsonUtils.getObjectFromJsonNode(valueNode,
ExpertiseFactory.getExpertise(beingByName).getClass());
return being;
}
}
In this way I was able to solve the above problem.
I am consuming a REST Api with RestTemplate. The response I'm getting from the API has lots of nested objects. Here's a little snippet as an example:
"formularios": [
{
"form_data_id": "123006",
"form_data": {
"form_data_id": "123006",
"form_id": "111",
"efs": {
"1": {},
"2": "{\"t\":\"c\",\"st\":\"m\",\"v\":[{\"id\":\"3675\",\"l\":\"a) Just an example\",\"v\":\"1\"},{\"id\":\"3676\",\"l\":\"b) Another example.\",\"v\":\"2\"}]}"
}
}
The problem I'm having is that most of the times the "1" actually has content, just like "2", and the jackson just parses it as a String on the object "efs". But sometimes, just like in the code snippet, the API sends it empty, and jackson takes it as an Object, which gives me an error that says something about START_OBJECT (can't remember the exact error, but it's not important for this question).
So I decided to make a custom deserializer so when jackson reads "1", it ignores the empty object and just parses it as a null string.
Here's my custom deserializer:
public class CustomDeserializer extends StdDeserializer<Efs> {
public CustomDeserializer(Class<Efs> t) {
super(t);
}
#Override
public Efs deserialize(JsonParser jp, DeserializationContext dc)
throws IOException, JsonProcessingException {
String string1 = null;
String string2 = null;
JsonToken currentToken = null;
while ((currentToken = jp.nextValue()) != null) {
if (currentToken.equals(JsonToken.VALUE_STRING)) {
if (jp.getCurrentName().equals("1")) {
string1 = jp.getValueAsString();
} else {
string2 = jp.getValueAsString();
}
} else {
if (jp.getCurrentName().equals("2")) {
string2 = jp.getValueAsString();
}
}
}
return new Efs(string1, string2);
}
}
And this is the way I'm using it when receiving the response from the API:
ObjectMapper mapper = new ObjectMapper();
SimpleModule mod = new SimpleModule("EfsModule");
mod.addDeserializer(Efs.class, new CustomDeserializer(Efs.class));
mapper.registerModule(mod);
List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
MappingJackson2HttpMessageConverter jsonMessageConverter = new MappingJackson2HttpMessageConverter();
jsonMessageConverter.setObjectMapper(mapper);
messageConverters.add(jsonMessageConverter);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setMessageConverters(messageConverters);
I'm getting the error:
CustomDeserializer has no default (no arg) constructor
But I don't know exactly what I'm doing wrong nor how to solve it. Thanks for the help and apologies for the long question, I wanted to give as much context as possible.
There is also one trap that users can fall into (like my self). If you declare deserializer as a inner class (not a static nested class) like:
#JsonDeserialize(using = DomainObjectDeserializer.class)
public class DomainObject {
private String key;
public class DomainObjectDeserializer extends StdDeserializer<DomainObject> {
public DomainObjectDeserializer() {
super(DomainObject.class);
}
#Override
public DomainObject deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// code
}
}
}
Jackson uses the Class#getDeclaredConstructor() with no argument (method accepts vararg) which means: give me a default (no argument) constructor. Code above will throw exception when Jackson tries to create DomainObjectDeserializer because javac generates the constructor that accepts enclosing class reference. Technically speaking DomainObjectDeserializer does not have a default constructor.
For a curiosity sake you can execute DomainObjectDeserializer.class.getDeclaredConstructors() and ensure that method does return single element array containing constructor definition with enclosing class reference.
The DomainObjectDeserializer should be declared as a static class.
Here is a good answer to read in more details.
It is required that you have a default constructor without arguments.
What you can do is create one (or replace the other one if you don't really need it):
public class CustomDeserializer extends StdDeserializer<Efs> {
public CustomDeserializer() {
super(Efs.class);
}
...
}
I have a large JSON file that is made up of structures that I am mapping into POJOs, and then storing in a Collection. The structure is similar to this:
[
{
"id": 1234,
"file": "C:\\Programs\\Program1.exe",
"exists": true
}
{
"id": 5678,
"file": "C:\\Programs\\Program2.exe",
"exists": false
}
...
]
Using the Jackson streaming API I have got all these structures read, and the POJOs stored in a Collection successfully. My POJO class looks like this:
public class Programs
{
#JsonProperty("id")
private Integer id;
#JsonProperty("file")
private String file;
#JsonProperty("exists")
private Boolean exists;
#JsonGetter("id")
public Integer getId()
{
return id;
}
#JsonGetter("file")
public String getFile()
{
return file;
}
#JsonGetter("exists")
public Boolean getExists()
{
return exists;
}
}
However, I want to omit any structures that have "exists" set to false during the deserialization process so that no POJO is ever created for them. So I wrote a custom deserializer with the help of this SO question [ How do I call the default deserializer from a custom deserializer in Jackson ], with my overridden deserialize looking like:
#Override
public Programs deserialize(JsonParser parser, DeserializationContext context)
throws IOException
{
Programs programs = (Programs)defaultDeserializer.deserialize(parser, context);
if (!programs.getExists())
{
throw context.mappingException("[exists] value is false.");
}
return programs;
}
However, when I run some unit tests, I get the following error:
"Can not deserialize instance of java.util.ArrayList out of START_OBJECT token"
message was "Class com.myprogram.serializer.ProgramsJsonDeserializer
has no default (no arg) constructor"
(Adding a no arg constructor gives the error that StdDeserializer does not have a default constructor.)
Is this the correct approach to achieving what I am trying to do? And does anyone know why I get this error message?
I want to omit any structures that have "exists" set to false during
the deserialization process so that no POJO is ever created for them.
I think your objective is to retrieve a list of Programs instance that only have exists set to true after derserialization. A customized CollectionDeserializer to filter those unwanted instance may help:
public class ProgramsCollectionHandler extends SimpleModule {
private static class ProgramsCollectionDeserializer extends CollectionDeserializer {
public ProgramsCollectionDeserializer(CollectionDeserializer deserializer) {
super(deserializer);
}
private static final long serialVersionUID = 1L;
#Override
public Collection<Object> deserialize(JsonParser parser, DeserializationContext context)
throws IOException, JsonProcessingException {
Collection<Object> result = super.deserialize(parser, context);
Collection<Object> filteredResult = new ArrayList<Object>();
for (Object o : result) {
if (o instanceof Programs) {
final Programs programs = (Programs) o;
if (programs.exists) {
filteredResult.add(programs);
}
}
}
return filteredResult;
}
#Override
public CollectionDeserializer createContextual(
DeserializationContext context,
BeanProperty property) throws JsonMappingException {
return new ProgramsCollectionDeserializer(super.createContextual(context, property));
}
}
private static final long serialVersionUID = 1L;
#Override
public void setupModule(Module.SetupContext context) {
super.setupModule(context);
context.addBeanDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyCollectionDeserializer(
DeserializationConfig config, CollectionType type,
BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
if (deserializer instanceof CollectionDeserializer) {
return new ProgramsCollectionDeserializer(
(CollectionDeserializer) deserializer);
}
return super.modifyCollectionDeserializer(config, type,
beanDesc, deserializer);
}
});
}
}
After that, your can register it into your object mapper:
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new ProgramsCollectionHandler());
"Can not deserialize instance of java.util.ArrayList out of
START_OBJECT token" message was "Class
com.myprogram.serializer.ProgramsJsonDeserializer has no default (no
arg) constructor"
(Adding a no arg constructor gives the error that StdDeserializer does not have a default constructor.)
This may be because your constructor cannot be accessed. For example, your deserializer is implemented as a non-static inner class.
I want to write json deserializer on class Type so that when Type is deserialized from given json based on name it maps value (of type interface Being) to its current implementation based on some factory method that returns correct class name based on name, and populates remaining class without any explicit deserialization and without creating object of TigerBeing or HumanBeing explicitly using new.
I tried to use #jsonCreator but there i have to initialize entire HumanBeing or TigerBeing using new and passing all json in constructor. I need auto mapping for types further used as further pojo can be quite complex.
{type:[{
"name": "Human",
"value": {
"height":6,
"weight":100,
"languages":["spanish","english"]
}
},
{
"name":"Tiger",
"value":{
"extinct":1,
"found":["Asia", "America", "Europe", "Africa"]
}
}
]}
I have:
public class Type {
String name;
Being value;
}
public interface Being {
}
public class TigerBeing implements Being {
Integer extinct;
String[] found;
}
public class HumanBeing implement Being {
Integer height;
Integer weight;
String[] languages;
}
import java.io.IOException;
public class BeingDeserializer extends JsonDeserializer<Being> {
#Override
public Expertise deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonMappingException {
JsonNode node = jp.getCodec().readTree(jp);
String beingName = node.get("name").asText();
JsonNode valueNode = node.get("value");
BeingName beingByName = ExpertiseName.getBeingByName(beingName);
if(beingByName ==null) {
throw new JsonMappingException("Invalid Being " + beingName);
}
Being being = JsonUtils.getObjectFromJsonNode(valueNode,
ExpertiseFactory.getExpertise(beingByName).getClass());
return being;
}
}
In this way I was able to solve the above problem.
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.