Testing custom JsonDeserializer in Jackson / SpringBoot

Tatiana Didik :

I am trying to write a unit test to a custom deserializer that is instantiated using a constructor with an @Autowired parameter and my entity marked with @JsonDeserialize. It works fine in my integration tests where a MockMvc brings up spring serverside.

However with tests where objectMapper.readValue(...) is being called, a new instance of deserializer using default constructor with no parameters is instantiated. Even though

@Bean
public MyDeserializer deserializer(ExternalObject externalObject) 

instantiates wired version of deserializer, real call is still passed to empty constructor and context is not being filled up.

I tried manually instantiating of a deserializer instance and registering it in ObjectMapper, but it only works if I remove @JsonDeserialize from my entity class (and it breaks my integration tests even if I do the same in my @Configuration class.) - looks related to this: https://github.com/FasterXML/jackson-databind/issues/1300

I can still test the deserializer behavior calling deserializer.deserialize(...) directly, but this approach doesn't work for me in tests that are not Deserializer's unit tests...

UPD: working code below

import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.cfg.HandlerInstantiator;
import com.github.tomakehurst.wiremock.common.Json;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;

import java.io.IOException;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;

@JsonTest
@RunWith(SpringRunner.class)
public class JacksonInjectExample {
    private static final String JSON = "{\"field1\":\"value1\", \"field2\":123}";

    public static class ExternalObject {
        @Override
        public String toString() {
            return "MyExternalObject";
        }
    }

    @JsonDeserialize(using = MyDeserializer.class)
    public static class MyEntity {
        public String field1;
        public String field2;
        public String name;

        public MyEntity(ExternalObject eo) {
            name = eo.toString();
        }

        @Override
        public String toString() {
            return name;
        }
    }

    @Component
    public static class MyDeserializer extends JsonDeserializer<MyEntity> {

        @Autowired
        private ExternalObject external;

        public MyDeserializer() {
            SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);
        }

        public MyDeserializer(@JacksonInject final ExternalObject external) {
            this.external = external;
        }

        @Override
        public MyEntity deserialize(JsonParser p, DeserializationContext ctxt) throws IOException,
            JsonProcessingException {
            return new MyEntity(external);
        }
    }

    @Configuration
    public static class TestConfiguration {
        @Bean
        public ExternalObject externalObject() {
            return new ExternalObject();
        }

        @Bean
        public MyDeserializer deserializer(ExternalObject externalObject) {
            return new MyDeserializer(externalObject);
        }
    }

    @Test
    public void main() throws IOException {
        HandlerInstantiator hi = mock(HandlerInstantiator.class);
        MyDeserializer deserializer = new MyDeserializer();
        deserializer.external = new ExternalObject();
        doReturn(deserializer).when(hi).deserializerInstance(any(), any(), eq(MyDeserializer.class));
        final ObjectMapper mapper = Json.getObjectMapper();
        mapper.setHandlerInstantiator(hi);

        final MyEntity entity = mapper.readValue(JSON, MyEntity.class);
        Assert.assertEquals("MyExternalObject", entity.name);
    }
}
Jörn Horstmann :

Very interesting question, it made me wonder how autowiring into jackson deserializers actually works in a spring application. The jackson facility that is used seems to be the HandlerInstantiator interface, which is configured by spring to the SpringHandlerInstantiator implementation, which just looks up the class in the application context.

So in theory you could setup an ObjectMapper in your unit test with your own (mocked) HandlerInstantiator, returning a prepared instance from deserializerInstance(). It seems to be fine to return null for other methods or when the class parameter does not match, this will cause jackson to create the instance on its own.

However, I do not think this is a good way to unit test deserialization logic, as the ObjectMapper setup is necessarily different from what is used during actual application execution. Using the JsonTest annotation as suggested in Anton's answer would be a much better approach, as you are getting the same json configuration that would be used during runtime.

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=415675&siteId=1