Right way to write JSON deserializer in Spring or extend it
JavaJsonSpringJacksonDeserializationJava Problem Overview
I am trying to write a custom JSON deserializer in Spring. I want to use default serializer for most part of fields and use a custom deserializer for few properties. Is it possible? I am trying this way because, most part of properties are values, so for these I can let Jackson use default deserializer; but few properties are references, so in the custom deserializer I have to query a database for reference name and get reference value from database.
I'll show some code if needed.
Java Solutions
Solution 1 - Java
I've searched a lot and the best way I've found so far is on this article:
Class to serialize
package net.sghill.example;
import net.sghill.example.UserDeserializer
import net.sghill.example.UserSerializer
import org.codehaus.jackson.map.annotate.JsonDeserialize;
import org.codehaus.jackson.map.annotate.JsonSerialize;
@JsonDeserialize(using = UserDeserializer.class)
public class User {
private ObjectId id;
private String username;
private String password;
public User(ObjectId id, String username, String password) {
this.id = id;
this.username = username;
this.password = password;
}
public ObjectId getId() { return id; }
public String getUsername() { return username; }
public String getPassword() { return password; }
}
Deserializer class
package net.sghill.example;
import net.sghill.example.User;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.ObjectCodec;
import org.codehaus.jackson.map.DeserializationContext;
import org.codehaus.jackson.map.JsonDeserializer;
import java.io.IOException;
public class UserDeserializer extends JsonDeserializer<User> {
@Override
public User deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
ObjectCodec oc = jsonParser.getCodec();
JsonNode node = oc.readTree(jsonParser);
return new User(null, node.get("username").getTextValue(), node.get("password").getTextValue());
}
}
Edit: Alternatively you can look at this article which uses new versions of com.fasterxml.jackson.databind.JsonDeserializer.
Solution 2 - Java
I was trying to @Autowire
a Spring-managed service into my Deserializer
. Somebody tipped me off to Jackson using the new
operator when invoking the serializers/deserializers. This meant no auto-wiring of Jackson's instance of my Deserializer
. Here's how I was able to @Autowire
my service class into my Deserializer
:
context.xml
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper" ref="objectMapper" />
</bean>
</mvc:message-converters>
</mvc>
<bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean">
<!-- Add deserializers that require autowiring -->
<property name="deserializersByType">
<map key-type="java.lang.Class">
<entry key="com.acme.Anchor">
<bean class="com.acme.AnchorDeserializer" />
</entry>
</map>
</property>
</bean>
Now that my Deserializer
is a Spring-managed bean, auto-wiring works!
AnchorDeserializer.java
public class AnchorDeserializer extends JsonDeserializer<Anchor> {
@Autowired
private AnchorService anchorService;
public Anchor deserialize(JsonParser parser, DeserializationContext context)
throws IOException, JsonProcessingException {
// Do stuff
}
}
AnchorService.java
@Service
public class AnchorService {}
Update: While my original answer worked for me back when I wrote this, @xi.lin's response is exactly what is needed. Nice find!
Solution 3 - Java
With Spring MVC 4.2.1.RELEASE, you need to use the new Jackson2 dependencies as below for the Deserializer to work.
Dont use this
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.9.12</version>
</dependency>
Use this instead.
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.2.2</version>
</dependency>
Also use com.fasterxml.jackson.databind.JsonDeserializer
and com.fasterxml.jackson.databind.annotation.JsonDeserialize
for the deserialization and not the classes from org.codehaus.jackson
Solution 4 - Java
- If you wanna override default deserialiser for specific properties, you can mark the properties and the deserialiser you want to use, eg:
--
// root json object
public class Example {
private String name;
private int value;
// You will implement your own deserilizer/Serializer for the field below//
@JsonSerialize(converter = AddressSerializer.class)
@JsonDeserialize(converter = AddressDeserializer.class)
private String address;
}
Here is a complete example.
- If you want to use non spring managed object mapper in Spring application context and configure the serialisers/deserialisers to use spring managed services to query the database, that can be achieved by telling Jackson to use
Spring Handler instantiator
to create instances of Deserialisers/Serialisers.
In your application context config, create ObjectMapper
bean with SpringHandlerInstantiator
eg:
@Autowired
ApplicationContext applicationContext;
@Bean
public ObjectMapper objectMapper(){
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.handlerInstantiator(handlerInstantiator());
// add additional configs, etc.. here if needed
return builder.build();
}
@Bean
public HandlerInstantiator handlerInstantiator(){
return new SpringHandlerInstantiator(applicationContext.getAutowireCapableBeanFactory());
}
Then you can @Autowire above objectMapper to deserialise jsons:
@Autowired
ObjectMapper objectMapper;
public Something readSomething(...){
...
Something st = objectMapper.readValue(json, Something.class);
...
return st;
}
Regardless of the deserialiser you want to use, ie: field or class, you can use spring context in the deserialiser, meaning that you can @Autowire your services or any spring bean managed by the same ApplicationContext
into it.
public class MyCustomDeserialiser extends ..{
@Autowired;
MyService service;
@AutoWired
SomeProperties properties;
@Override
public MyValue deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
....
}
...
}
Additionally, you can find an example of Jackson deserialiser here.