11

Is it possible to setup Jersey using Jackson for serialization/deserialization using multiple configured ObjectMappers?

What I would like to be able to do is register a "default" Jackson ObjectMapper and then have the ability to register another feature which provides an ObjectMapper with some specialized configuration which under certain circumstance will "override" the "default" ObjectMapper.

For example, this ContextResolver would be for the "default" mapper:

@Provider
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class JacksonMapperProvider implements ContextResolver<ObjectMapper> {
    private final ObjectMapper mObjectMapper;

    public JacksonMapperProvider() {
        mObjectMapper = createMapper();
    }

    protected abstract ObjectMapper createMapper() {
        ObjectMapper mapper = createMapper();

        return mapper
            .setSerializationInclusion(Include.ALWAYS)
            .configure(JsonParser.Feature.ALLOW_COMMENTS, true)
            .configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true)
            .configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true)
            .configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
    }

    @Override
    public ObjectMapper getContext(Class<?> type) {
        return mObjectMapper;
    }
}

And this ContextResolver would be to override the "default" mapper:

@Provider
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class SpecializedMapperProvider implements ContextResolver<ObjectMapper> {
    private final ObjectMapper mObjectMapper;

    public SpecializedMapperProvider() {
        mObjectMapper = createMapper();
    }

    protected abstract ObjectMapper createMapper() {
        ObjectMapper mapper = createMapper();

        return mapper
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"))
            .registerModule(new SpecializedModule1())
            .registerModule(new SpecializedModule2());
    }

    @Override
    public ObjectMapper getContext(Class<?> type) {
        if(SomeType.isAssignableFrom(type)) {
            return mObjectMapper;
        }
        return null;
    }
}

I see in the JacksonJsonProvider code that Jackson supports ObjectMapper provider injection/resolution. However, in practice, what I am seeing is that the "order" of the providers seems random (I'm guessing it's not, but I can't sort out how to control the order). Sometimes the "override" comes before the "default" and everything works, but on the next server startup the order changes.

I have attempted to get this to work in a number of ways including:

  • Registering the ContextResolver<ObjectMapper> implementations manually (in differing orders)
  • Registering the ContextResolver<ObjectMapper> implementations via @Provider annotations
  • Specifying a priority when registering

I am using the following:

  • Jersey 2.8
  • Jackson 2.3.3

Perhaps I am taking a completely incorrect approach?
Is there a better way to achieve what I am trying to do? Maybe I should just define two separate JAX-RS applications and have a single ObjectMapper configuration for each?

cmatthews.dickson
  • 687
  • 2
  • 8
  • 17
  • What I found is once you have `ObjectMapper defaultMapper = return new ObjectMapper()`, it is not the same as default `JacksonJsonProvider` instance. Atleast not for `JacksonJaxbJsonProvider`. – ulab Apr 11 '16 at 13:16

2 Answers2

4

You can configure the order of providers, but it would actually be best to use one provider in this situation:

@Provider
public class JacksonMapperProvider implements ContextResolver<ObjectMapper> {
    private final ObjectMapper defaultMapper;
    private final ObjectMapper specializedMapper;

    public JacksonMapperProvider() {
        defaultMapper = createDefaultMapper();
        specializedMapper = createSpecializedMapper();
    }

    private static ObjectMapper createDefaultMapper() {
        return new ObjectMapper()
            .setSerializationInclusion(Include.ALWAYS)
            .configure(JsonParser.Feature.ALLOW_COMMENTS, true)
            .configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true)
            .configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true)
            .configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
    }

    private static ObjectMapper createSpecializedMapper() {
        return new ObjectMapper()
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"))
            .registerModule(new SpecializedModule1())
            .registerModule(new SpecializedModule2());
    }

    @Override
    public ObjectMapper getContext(Class<?> type) {
        if (SomeType.isAssignableFrom(type)) {
            return specializedMapper;
        }
        else {
            return defaultMapper;
        }
    }
}
Alden
  • 6,553
  • 2
  • 36
  • 50
  • 2
    Thanks. This is what I did initially to get it working, however, I was hoping to keep things separate and agnostic of different modules/areas of the application and to avoid a potential "mega provider" that could result from different mappers being needed. Can you elaborate on why _"it would be best to use one provider in this situation"_? – cmatthews.dickson May 12 '14 at 23:31
  • I had a similar problem made worse because we wanted to use MixIns for the Jackson annotations rather that put them on the actual classes. We solved our problem by creating our own `@JsonMixin` annotation and then scanning for all the annotated objects at run time. You could do something similar to configure your `ObjectMapper` dynamically and maintain only one. – Baldy May 14 '14 at 23:13
0

The newest way is

new ObjectMapper().configure(com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);

to get ALLOW_UNQUOTED_FIELD_NAMES in recent versions of jackson.

Faraz
  • 6,025
  • 5
  • 31
  • 88