Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom ObjectMapper with Jersey 2.2 and Jackson 2.1

I am struggling with a REST application with Grizzly, Jersey and Jackson, because Jersey ignores my custom ObjectMapper.

POM dependencies:

<dependencies>
    <dependency>
        <groupId>org.glassfish.jersey.containers</groupId>
        <artifactId>jersey-container-grizzly2-servlet</artifactId>
        <version>2.2</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.jaxrs</groupId>
        <artifactId>jackson-jaxrs-json-provider</artifactId>
        <version>2.1.4</version>
    </dependency>
</dependencies>

Resulting versions are: Grizzly 2.3.3, Jackson 2.1.4 and Jersey 2.2.

Main class (I want explicit registration of Jersey components):

public class Main {
    public static void main(String[] args) {
        try {
            ResourceConfig rc = new ResourceConfig();
            rc.register(ExampleResource.class);
            rc.register(ObjectMapperResolver.class);

            HttpHandler handler = ContainerFactory.createContainer(
                    GrizzlyHttpContainer.class, rc);

            URI uri = new URI("http://0.0.0.0:8080/");

            HttpServer server = GrizzlyHttpServerFactory.createHttpServer(uri);

            ServerConfiguration config = server.getServerConfiguration();
            config.addHttpHandler(handler, "/");

            server.start();
            System.in.read();

        } catch (ProcessingException | URISyntaxException | IOException e) {
            throw new Error("Unable to create HTTP server.", e);
        }
    }
}

ContextResolver for ObjectMapper:

@Provider
@Produces(MediaType.APPLICATION_JSON)
public class ObjectMapperResolver implements ContextResolver<ObjectMapper> {

    private final ObjectMapper mapper;

    public ObjectMapperResolver() {
        System.out.println("new ObjectMapperResolver()");
        mapper = new ObjectMapper();
        mapper.enable(SerializationFeature.INDENT_OUTPUT);
    }

    @Override
    public ObjectMapper getContext(Class<?> type) {
        System.out.println("ObjectMapperResolver.getContext(...)");
        return mapper;
    }

}

Neither ObjectMapperResolver constructor nor getContext get called. What am I missing? I would prefer to use Jersey 2.2 and Jackson 2.1, because it is a dependency for another lib.

A full example can be found on GitHub: https://github.com/svenwltr/example-grizzly-jersey-jackson/tree/stackoverflow

like image 391
svenwltr Avatar asked Sep 18 '13 12:09

svenwltr


People also ask

Is ObjectMapper thread-safe Jackson?

Jackson's ObjectMapper is completely thread safe and should not be re-instantiated every time #2170.

Should Jackson ObjectMapper be static?

Yes, that is safe and recommended.

Should I reuse ObjectMapper?

Note that copy() operation is as expensive as constructing a new ObjectMapper instance: if possible, you should still pool and reuse mappers if you intend to use them for multiple operations.

Does spring boot have Jackson ObjectMapper?

Overview. When using JSON format, Spring Boot will use an ObjectMapper instance to serialize responses and deserialize requests. In this tutorial, we'll take a look at the most common ways to configure the serialization and deserialization options. To learn more about Jackson, be sure to check out our Jackson tutorial.


4 Answers

The following solution applies to the following stack (as in... this is the setup I've used to test it)

Jersey 2.12, Jackson 2.4.x

I'm adding my message w/ the solution I've come up with on this post since it was quite relevant for the many Google searches I've put in today... It is a cumbersome solution to what I believe to be an even more cumbersome problem.

1. Make sure your maven configuration CONTAINS the jackson-jaxrs-json-provider dependency:

<dependency>     <groupId>com.fasterxml.jackson.jaxrs</groupId>     <artifactId>jackson-jaxrs-json-provider</artifactId>     <version>2.4.1</version> </dependency> 

2. Make sure your maven configuration DOESN'T CONTAIN the jersey-media-json-jackson dependency:

<dependency>     <groupId>org.glassfish.jersey.media</groupId>     <artifactId>jersey-media-json-jackson</artifactId> </dependency> 

3. Create a @Provider component extending com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider like so:

import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;  import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.ext.Provider;  @Provider @Produces(MediaType.APPLICATION_JSON) public class CustomJsonProvider extends JacksonJaxbJsonProvider {      private static ObjectMapper mapper = new ObjectMapper();      static {         mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);         mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);         mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);         mapper.enable(SerializationFeature.INDENT_OUTPUT);      }      public CustomJsonProvider() {         super();         setMapper(mapper);     } } 

As you can observe this is also where we define the custom instance of com.fasterxml.jackson.databind.ObjectMapper

4. Extend javax.ws.rs.core.Feature via MarshallingFeature like so:

import javax.ws.rs.core.Feature; import javax.ws.rs.core.FeatureContext; import javax.ws.rs.ext.MessageBodyReader; import javax.ws.rs.ext.MessageBodyWriter;  public class MarshallingFeature implements Feature {      @Override     public boolean configure(FeatureContext context) {         context.register(CustomJsonProvider.class, MessageBodyReader.class, MessageBodyWriter.class);         return true;     } } 

5. You need to register this custom provider like so, provided you configure your application via org.glassfish.jersey.server.ResourceConfig like so:

import org.glassfish.jersey.server.ResourceConfig; ...  public class MyApplication extends ResourceConfig {      public MyApplication() {          ...         register(MarshallingFeature.class);         ...      }  } 

Other notes and observations:

  1. This solution applies whether you're using javax.ws.rs.core.Response to wrap your controller's responses or not.
  2. Please make sure you carefully take into consideration (copy/paste) the following code snippets since the only "non-mandatory" so to speak bits are the ones regarding the custom configuration of the com.fasterxml.jackson.databind.ObjectMapper.

@jcreason

Sorry for dropping the ball on this one @jcreason, I hope you're still curios. So I checked out the code from last year and this is what I came up w/ to provide a custom mapper.

The problem was that during feature initalization any custom object mappers get disabled by some code in

org.glassfish.jersey.jackson.JacksonFeature:77 (jersey-media-json-jackson-2.12.jar)

// Disable other JSON providers. context.property(PropertiesHelper.getPropertyNameForRuntime(InternalProperties.JSON_FEATURE, config.getRuntimeType()), JSON_FEATURE); 

But this feature only gets registered by this component

org.glassfish.jersey.jackson.internal.JacksonAutoDiscoverable

if (!context.getConfiguration().isRegistered(JacksonFeature.class)) {     context.register(JacksonFeature.class); } 

So what I did was to register my own feature which registeres my own object mapper provider and drops in a trip wire stopping org.glassfish.jersey.jackson.JacksonFeature from being registered and overriding my object mapper...

import com.fasterxml.jackson.jaxrs.base.JsonMappingExceptionMapper; import com.fasterxml.jackson.jaxrs.base.JsonParseExceptionMapper;  import org.glassfish.jersey.internal.InternalProperties; import org.glassfish.jersey.internal.util.PropertiesHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory;  import javax.ws.rs.core.Configuration; import javax.ws.rs.core.Feature; import javax.ws.rs.core.FeatureContext; import javax.ws.rs.ext.MessageBodyReader; import javax.ws.rs.ext.MessageBodyWriter;  public class MarshallingFeature implements Feature {      private final static String JSON_FEATURE = MarshallingFeature.class.getSimpleName();      @Override     public boolean configure(FeatureContext context) {        context.register(JsonParseExceptionMapper.class);       context.register(JsonMappingExceptionMapper.class);       context.register(JacksonJsonProviderAtRest.class, MessageBodyReader.class, MessageBodyWriter.class);        final Configuration config = context.getConfiguration();       // Disables discoverability of org.glassfish.jersey.jackson.JacksonFeature       context.property(           PropertiesHelper.getPropertyNameForRuntime(InternalProperties.JSON_FEATURE,                                                      config.getRuntimeType()), JSON_FEATURE);        return true;     } } 

And here is the custom object mapper provider...

import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;  import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.ext.Provider;  @Provider @Produces(MediaType.APPLICATION_JSON) public class JacksonJsonProviderAtRest extends JacksonJaxbJsonProvider {      private static ObjectMapper objectMapperAtRest = new ObjectMapper();      static {         objectMapperAtRest.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);         objectMapperAtRest.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);         objectMapperAtRest.configure(SerializationFeature.INDENT_OUTPUT, true); // Different from default so you can test it :)         objectMapperAtRest.setSerializationInclusion(JsonInclude.Include.ALWAYS);     }      public JacksonJsonProviderAtRest() {         super();         setMapper(objectMapperAtRest);     } } 
like image 162
Filip Avatar answered Oct 05 '22 23:10

Filip


I found a solution. I had to instantiate the Jackson Provider by myself and set my custom ObjectMapper. A working example can be found on GitHub: https://github.com/svenwltr/example-grizzly-jersey-jackson/tree/stackoverflow-answer

I deleted my ObjectMapperResolver and modified my main method:

public class Main {     public static void main(String[] args) {         try {             // create custom ObjectMapper             ObjectMapper mapper = new ObjectMapper();             mapper.enable(SerializationFeature.INDENT_OUTPUT);              // create JsonProvider to provide custom ObjectMapper             JacksonJaxbJsonProvider provider = new JacksonJaxbJsonProvider();             provider.setMapper(mapper);              // configure REST service             ResourceConfig rc = new ResourceConfig();             rc.register(ExampleResource.class);             rc.register(provider);              // create Grizzly instance and add handler             HttpHandler handler = ContainerFactory.createContainer(                     GrizzlyHttpContainer.class, rc);             URI uri = new URI("http://0.0.0.0:8080/");             HttpServer server = GrizzlyHttpServerFactory.createHttpServer(uri);             ServerConfiguration config = server.getServerConfiguration();             config.addHttpHandler(handler, "/");              // start             server.start();             System.in.read();          } catch (ProcessingException | URISyntaxException | IOException e) {             throw new Error("Unable to create HTTP server.", e);         }     } } 
like image 38
svenwltr Avatar answered Oct 05 '22 23:10

svenwltr


I figured this out, based on a bit of tinkering.

The issue appears to be in Jersey's feature autodetection mechanism. If you rely on Jersey to load the JacksonJaxbJsonProvider, then the custom context provider for your ObjectMapper is ignored. If, instead, you manually register the feature, it works. I hypothesize that this has to do with the autodetected provider being loaded into a different context scope, but as for a solution, here's what I ended up with. Note that I wrapped it into a feature, you should be able to register it directly with your application without a problem.

public final class RequestMappingFeature implements Feature {

    @Override
    public boolean configure(final FeatureContext context) {

        context.register(ObjectMapperProvider.class);

        // If you comment out this line, it stops working.
        context.register(JacksonJaxbJsonProvider.class);

        return true;
    }
}

UPDATE November 2017: Things have changed a bit in the Jersey2 world. If the above doesn't work, try this:

The new method of providing your own ObjectMapper now looks like this:

public final class JacksonFeature implements Feature {

    private static final ObjectMapper MAPPER;

    static {

        // Create the new object mapper.
        MAPPER = new ObjectMapper();

        // Enable/disable various configuration flags.
        MAPPER.configure(
                DeserializationFeature.READ_ENUMS_USING_TO_STRING, true);

        // ... Add your own configurations here.

    }
    @Override
    public boolean configure(final FeatureContext context) {
        JacksonJaxbJsonProvider provider = new JacksonJaxbJsonProvider(
                MAPPER, DEFAULT_ANNOTATIONS);
        context.register(provider);

        return true;
    }
}
like image 22
krotscheck Avatar answered Oct 05 '22 23:10

krotscheck


Please do this:

1) add pom.xml dependency

<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jackson</artifactId>
    <version>2.2</version>
</dependency>

2) register JacksonFeature in the Main.java

public class Main {

    public static void main(String[] args) {
        try {
            ResourceConfig rc = new ResourceConfig();
            rc.register(ExampleResource.class);
            rc.register(ObjectMapperResolver.class);
            rc.register(JacksonFeature.class);

            HttpHandler handler = ContainerFactory.createContainer(
                    GrizzlyHttpContainer.class, rc);

            URI uri = new URI("http://0.0.0.0:8080/");

            HttpServer server = GrizzlyHttpServerFactory.createHttpServer(uri);

            ServerConfiguration config = server.getServerConfiguration();
            config.addHttpHandler(handler, "/");

            server.start();
            System.in.read();

        } catch (ProcessingException | URISyntaxException | IOException e) {
            throw new Error("Unable to create HTTP server.", e);
        }
    }
}

3) Use org.codehaus.jackson.map.ObjectMapper in your resource

import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;

import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig.Feature;

@Provider
@Produces(MediaType.APPLICATION_JSON)
public class ObjectMapperResolver implements ContextResolver<ObjectMapper> {

    private final ObjectMapper mapper;

    public ObjectMapperResolver() {
        System.out.println("new ObjectMapperResolver()");
        mapper = new ObjectMapper();
        mapper.enable(Feature.INDENT_OUTPUT);
    }

    @Override
    public ObjectMapper getContext(Class<?> type) {
        System.out.println("ObjectMapperResolver.getContext(...)");
        return mapper;
    }
}
like image 30
alexey Avatar answered Oct 05 '22 22:10

alexey