Jackson ObjectMapper using custom Serializers and Deserializers

22,439

Solution 1

From the answers & comments provided here, I recently redesigned the class to use builders for both the Module and the ObjectMapper. This allowed me to provide mocks and check that the correct (de)serializers were added to the module and then the module is registered to the object mapper as expected.

Object Mapper Builder:

public class ObjectMapperBuilder {
    ObjectMapper mapper;

    public ObjectMapperBuilder configure(final ObjectMapper mapper) {
        this.mapper = mapper;
        return this;
    }

    public ObjectMapperBuilder withModule(final Module module) {
        this.mapper.registerModule(module);
        return this;
    }

    public ObjectMapper build() {
        return this.mapper;
    }
}

Module Builder:

public class SimpleModuleBuilder {
    SimpleModule module;

    public SimpleModuleBuilder configure(final SimpleModule module) {
        this.module = module;
        return this;
    }

    public <X> SimpleModuleBuilder withSerializer(final Class<X> clazz, final JsonSerializer<X> serializer) {
        this.module.addSerializer(clazz, serializer);
        return this;
    }

    public <X> SimpleModuleBuilder withDeserializer(final Class<X> clazz, final JsonDeserializer<X> deserializer) {
        this.module.addDeserializer(clazz, deserializer);
        return this;
    }

    public SimpleModule build() {
        return this.module;
    }
}

And finally, the new JsonMapperFactory:

public class JsonMapperFactory {

    public static ObjectMapper configureObjectMapper(final ObjectMapper mapper, final SimpleModule module) {
        final SimpleModuleBuilder modulebuilder = new SimpleModuleBuilder();

        final SimpleModule configuredModule = modulebuilder.configure(module)
            .withSerializer(DateTime.class, new DateTimeSerializer())
            .withDeserializer(DateTime.class, new DateTimeDeserializer())
            .build();

        final ObjectMapperBuilder objectMapperBuilder = new ObjectMapperBuilder();
        return objectMapperBuilder.configure(mapper).withModule(configuredModule).build();
    }
}

The factory method is still used within Spring configuration, but the configuration now instantiates the blank Module and ObjectMapper before providing them to the factory methods that then configure them.

Solution 2

If JsonDeserializer (and DateTimeDeserializer too) was an interface, you could easily "JMock" it, pass mocked instance to JsonMapperFactory#createObjectMapper and then expect exactly 1 invocation of your custom "serialize" method; e.g.

DateTimeSerializer serializer = context.mock(DateTimeSerializer.class);
DateTimeDeserializer serializer = context.mock(DateTimeDeserializer.class);
ObjectMapper mapper = JacksonMapperFactory.createObjectMapper(deserializer, serializer);

exactly(1).of(jsonDeserializer).serialize(myDateTime,
  with(any(JsonGenerator.class),
  with(any(SerializerProvider.class)))

Being a concrete class, you can instead define a new (test-scoped) De/Serializer that extends your custom DateTime(De)serializer, and simply count invocation on that:

private static class DateTimeDeserializerWithCounter extends DateTimeDeserializer {
    public int counter = 0;

    @Override
    public DateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
        counter++;
        return super.deserialize(jsonParser, deserializationContext);
    }
}

@Test
public void usageTest(){
    //init mapper with the above DateTimeDeserializerWithCounter - see below
    mapper.readValue("...", DateTime.class);
    Assert.assertEquals(1, deserializer.counter);
}

Below a snapshot of a more "test-oriented" Factory:

//package visibility, to allow passing different De/Serializers while testing
static ObjectMapper createObjectMapper(JsonDeserializer deserializer, JsonSerializer serializer) {
    final SimpleModule module = new SimpleModule("customerSerializationModule", new Version(1, 0, 0, "static version"));
    module.addDeserializer(DateTime.class, deserializer);
    module.addSerializer(DateTime.class, serializer);

    final ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModule(module);
    return objectMapper;
}

//production method: no-args, as in the original version
public static ObjectMapper createObjectMapper() {
    return createObjectMapper(new DateTimeDeserializer(), new DateTimeSerializer());
}

Hope that helps.

Share:
22,439
Dan Temple
Author by

Dan Temple

Focussing on Java with a lean towards Testing (primarily Mocking) #SOreadytohelp

Updated on July 05, 2022

Comments

  • Dan Temple
    Dan Temple almost 2 years

    I've got a class that configures a Jackson ObjectMapper. It adds in some custom serializers and deserializers for my object types as follows:

    public class JsonMapperFactory {
        public static ObjectMapper createObjectMapper() {
            final SimpleModule module = new SimpleModule("customerSerializationModule", new Version(1, 0, 0, "static version"));
            addCustomDeserializersTo(module);
            addCustomSerializersTo(module);
    
            final ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.registerModule(module);
            return objectMapper;
        }
        private static void addCustomSerializersTo(final SimpleModule module) {
            module.addSerializer(DateTime.class, new DateTimeSerializer());
        }
        private static void addCustomDeserializersTo(final SimpleModule objectMapper) {
            objectMapper.addDeserializer(DateTime.class, new DateTimeDeserializer());
        }
    }
    

    I've tested my customer serializers within their own test classes, so in my test of this JsonMapperFactory class, I'm trying to simply check that the ObjectMapper created has the expected serializers (or deserializers) This could be achieve by introspecting the ObjectMapper, but it doesn't seem to have any mechanisms to do this.

    Does anyone know of a nice way to test that?

    For deserializers, I have the following:

    private void assertThatObjectMapperUsesCorrectDeserializer(final Class<?> typeClazz, final Class<?> deserializerClazz) throws JsonMappingException {
        final  DeserializationConfig deserializationConfig = this.objectMapper.getDeserializationConfig();
        final JsonDeserializer<Object> deserializer = this.objectMapper.getDeserializerProvider().findTypedValueDeserializer(deserializationConfig, javaTypeFor(typeClazz), null);
        assertThat(deserializer, is(instanceOf(deserializerClazz)));
    }
    private JavaType javaTypeFor(final Class<?> clazz) {
        return TypeFactory.type(clazz); //deprecated method :(
    }
    

    Which is quite verbose and uses deprecated methods.

    I'm yet to find a way to do a similar test for the serializers. So I've currently resorted to serializing an object and check it serializes correctly (essentially duplicating the serializer test)

    Any ideas are very welcome.

  • Dan Temple
    Dan Temple over 10 years
    I do like this idea, but I think it's testing the wrong thing. I am using Jackson as a proven library, so I am willing to accept that it works as I expect. Your solution relies on De/Serializing an object and checking that the Jackson library has called my de/serializers. So it is almost testing the Jackson library. I wonder if there is a way to either introspect the ObjectMapper, or redesign the creation to allow me to test it is created how I expect. I have been thinking about builder/factory classes which are then mocked in the tests.
  • Dan Temple
    Dan Temple over 10 years
    Your solution also opened my mind to the idea of redesigning the JsonMapperFactory class, which I hadn't given much thought to previously.