Right way to write JSON deserializer in Spring or extend it

JavaJsonSpringJacksonDeserialization

Java 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

  1. 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.

  1. 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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questiongc5View Question on Stackoverflow
Solution 1 - Javagc5View Answer on Stackoverflow
Solution 2 - JavaBeezView Answer on Stackoverflow
Solution 3 - JavaVimalKumarView Answer on Stackoverflow
Solution 4 - JavaDhanushkaView Answer on Stackoverflow