Using Jackson ObjectMapper with Jersey

jcstockdale picture jcstockdale · Dec 13, 2013 · Viewed 41k times · Source

I'm using Jersey 2.4 to create a simple REST interface that serves up a JSON object. My problem is that I'm trying to use the fasterxml Jackson annotations to control the output and this is not working for me. I have put the annotations into my bean class but they are ignored.

When I explicitly create an ObjectMapper and use this to stringify the Java bean, I get the output that I want, which respects the Jackson annotations. However, I would prefer that I don't have to do this step so that my resource class can simply return the bean and the Jersey framework takes care of stringifying it.

I have tried to solve this using the answer from Custom ObjectMapper with Jersey 2.2 and Jackson 2.1, however, this does not appear to work for me. I see that the ContextResolver is created but it is never called.

I have also spent many hours trying to solve this apparently simple problem. I have stripped this down to a very simple test case, which is shown below. I would appreciate any help at all in resolving this.

Resource Java class:

@Path("resource")
public class MainResource {

    public static class Foobar {
        @JsonIgnore
        private String foo = "foo";
        private String baa = "baa";
        private Map<String, List<? extends Number>> map = new HashMap<>();

        public Foobar() {
            map.put("even", Arrays.asList(new Integer[] { 2, 4, 6, 8, 10 }));
            map.put("odd", Arrays.asList(new Integer[] { 1, 3, 5, 7, 9 }));
            map.put("float", Arrays.asList(new Float[] { 1.1F, 2.2F, 3.3F }));
        }

        public String getFoo() {
            return foo;
        }

        public void setFoo(String foo) {
            this.foo = foo;
        }

        public String getBaa() {
            return baa;
        }

        public void setBaa(String baa) {
            this.baa = baa;
        }

        @JsonAnyGetter
        public Map<String, List<? extends Number>> getMap() {
            return map;
        }

        public void setMap(Map<String, List<? extends Number>> map) {
            this.map = map;
        }
    }

    private ObjectMapper om = new ObjectMapper();

    @GET
    @Path("get-object")
    @Produces(MediaType.APPLICATION_JSON)
    public Foobar getObject() {
        // In this method, I simply return the bean object but the WRONG JSON syntax is generated.
        return new Foobar();
    }

    @GET
    @Path("get-string")
    @Produces(MediaType.APPLICATION_JSON)
    public String getString() throws JsonProcessingException {
        // This method returns the RIGHT JSON syntax but I don't want to have to explicitly use the ObjectMapper.
        Foobar foobar = new Foobar();
        return om.writeValueAsString(foobar);
    }
}

web.xml:

<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

    <module-name>sample</module-name>

    <servlet>
        <servlet-name>Jersey Web Application</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>jersey.config.server.provider.packages</param-name>
            <param-value>ie.cit.nimbus.sample</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>Jersey Web Application</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

</web-app>

POM dependencies:

<dependencies>
    <dependency>
        <groupId>com.fasterxml.jackson.jaxrs</groupId>
        <artifactId>jackson-jaxrs-json-provider</artifactId>
        <version>2.3.0</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.ext</groupId>
        <artifactId>jersey-spring3</artifactId>
        <version>2.4.1</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.media</groupId>
        <artifactId>jersey-media-json-jackson</artifactId>
        <version>2.4.1</version>
    </dependency>
</dependencies>

Answer

Patrick Favre picture Patrick Favre · Jan 16, 2014

EDIT: Don't use the old approach below as it produces bugs (at least with with android device, see EDIT2 for more details). As of my tests, Jersey v2.6 seems to fix the problem with the @Provide, which approach did not work. I was able to get it work with this simple provider:

@Provider
public class JerseyMapperProvider implements ContextResolver<ObjectMapper> {
    private static ObjectMapper apiMapper = ObjectMapperManager.createMapperForApi();
    @Override
    public ObjectMapper getContext(Class<?> type)
    {
        return apiMapper;
    }
}

So please don't use my hack from below.


OLD APPROACH

Using

@Provider
public class MyObjectMapperProvider implements ContextResolver<ObjectMapper>

was not working for me (Jersey 2.4 & Jackson 2.3) and maybe this is due to a in the jackson provider reported bug in the code where the ContextResolver should be registered in JacksonJsonProvider.java (2.3rc1):

 @Override
protected ObjectMapper _locateMapperViaProvider(Class<?> type, MediaType mediaType)
{
    if (_providers != null) {
        ContextResolver<ObjectMapper> resolver = _providers.getContextResolver(ObjectMapper.class, mediaType);
        /* Above should work as is, but due to this bug
         *   [https://jersey.dev.java.net/issues/show_bug.cgi?id=288]
         * in Jersey, it doesn't. But this works until resolution of
         * the issue:
         */
        if (resolver == null) {
            resolver = _providers.getContextResolver(ObjectMapper.class, null);
        }
        if (resolver != null) {
            return resolver.getContext(type);
        }
    }
    return null;
}

But at least I cannot access https://jersey.dev.java.net/issues/show_bug.cgi?id=288, so I don't know what this bug is about.

But I found a workaround (a hack if you so will). Just extend JacksonJsonProvider with the proper annotation and return your ObjectMapper like this:

@Provider
@Consumes(MediaType.APPLICATION_JSON) // NOTE: required to support "non-standard" JSON variants
@Produces(MediaType.APPLICATION_JSON)
public class JacksonHackProvider extends JacksonJsonProvider {
    @Override
    protected ObjectMapper _locateMapperViaProvider(Class<?> type, MediaType mediaType) {
        return new MyCustomObjectMapper();
    }
}

No need to do anything it will register itself (check with log, it will register the first time you access a json rest service). This is now working for me, not elegant, but I gave up.

EDIT: Use with caution - Im experiencing a bug maybe related to this hack: Android volley cannot send a POST/PUT request with a request body, always getting 400 from the framework, I will investigate and report my findings.

EDIT2: This hack was indeed responsible for a generic 400 whenever an Android app with volley and OKHTTP client tried tried to do a POST or PUT request so don't use this - in my test jersey 2.6 seems to fix this so you can use @Provide approach