Search code examples
javajsonjacksonjerseyretrofit2

Using customized ObjectWriter with Jersey


I am developing a REST webservice. Jersey as jax-rs provider and Jackson for serialization/deserialization. I also develop the client based on Retrofit2.

My class hierarchy is provided by a third-party library and all classes descend from a root base class BaseObject. Some of those classes have undesirable getters, e.g. isEmpty, that I want to ignore on serialization (Note that it is important that they do not get serialized at all and using FAIL_ON_UNKNOWN_PROPERTIES on deserialization is not enough in my case).

I have used Jackson @JsonFilter on BaseClass using Mixins. To apply a filter, as far as I know, one has to use the following:

new ObjectMapper().writer(filterProvider).writeValueAsString...

Everything is ok up to here: the undesired property is successfully filtered from the produced json.

Now I have to configure Jersey and Retrofit2 to use my customized json serializer/deserializer.

For Jersey, serialization/deserialization can be configured using a Provider class that implements ContextResolver<ObjectMapper> and returning customized ObjectMapper in getContext(Class<?> type) method.

Similarly in Retrofit2, by using Retrofit.Builder().addConverterFactory(JacksonConverterFactory.create(objectMapper)), one can customize serialization/deserialization.

THE PROBLEM IS THAT new ObjectMapper().writer(filterProvider) is of type ObjectWriter and not of type ObjectMapper. How can I tell Jersey and Retrofit2 to use my customized ObjectWriter which uses my filters?


Solution

  • Since version 2.6 of Jackson it has the 'setFilterProvider' method for an ObjectMapper. I didn't try it but the documentation has the description for this: https://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/ObjectMapper.html#setFilterProvider-com.fasterxml.jackson.databind.ser.FilterProvider-. You can try i think because the description fits for your case.

    I built a test service with Jersey 2.7 and Jackson 2.9.5. it works fine but you have to know some tricks to run it.

    In pom.xml add Jersey and Jackson:

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.glassfish.jersey</groupId>
                <artifactId>jersey-bom</artifactId>
                <version>${jersey.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.glassfish.jersey.containers</groupId>
            <artifactId>jersey-container-servlet-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-json-jackson</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>${jackson.version}</version>
        </dependency>
    </dependencies>
    <properties>
        <jersey.version>2.7</jersey.version>
        <jackson.version>2.9.5</jackson.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    

    You have to define this dependence:

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

    it's mandatory.

    In web.xml you have to make the ref to configuration of your service:

    <web-app version="2.5"
         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_2_5.xsd">
    <servlet>
        <servlet-name>Jersey Web Application</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>org.glassfish.jersey.server.ResourceConfig</param-name>
            <param-value>com.home.MyApplication</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    

    MyApplication.java:

    package com.home;
    
    import org.glassfish.jersey.jackson.JacksonFeature;
    import org.glassfish.jersey.server.ResourceConfig;
    
    import javax.ws.rs.ApplicationPath;
    
    @ApplicationPath("/webapi")
    public class MyApplication extends ResourceConfig {
    
        public MyApplication() {
    
            register(ObjectMapperProvider.class);
            register(JacksonFeature.class);
            register(MyResource.class);
    
        }
    }
    

    With a custom ObjectMapperProvider you have to register a JacksonFeature.class because without it Jersey doesn't use the custom ObjectMapperProvider.

    ObjectMapperProvider.java:

    package com.home;
    
    import com.fasterxml.jackson.databind.DeserializationFeature;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.SerializationFeature;
    import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
    import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
    import javax.ws.rs.ext.ContextResolver;
    import javax.ws.rs.ext.Provider;
    
    @Provider
    public class ObjectMapperProvider implements ContextResolver<ObjectMapper>{
    
        final ObjectMapper defaultObjectMapper;
    
        public ObjectMapperProvider() {
            defaultObjectMapper = createDefaultMapper();
        }
    
        @Override
        public ObjectMapper getContext(Class<?> type) {return defaultObjectMapper;}
    
        public static ObjectMapper createDefaultMapper() {
    
            final ObjectMapper mapper = new ObjectMapper();
            mapper.enable(SerializationFeature.INDENT_OUTPUT);
            mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
            mapper.setFilters(new SimpleFilterProvider().addFilter("dataFilter", SimpleBeanPropertyFilter.serializeAllExcept("region", "city")));
            return mapper;
    
        }
    }
    

    To define a filter use the 'setFilters' methods. This method is deprecated but the Jersey's library which called 'jersey-hk2' doesn't know the new method 'setFilterProvider' and throws an exception. With the old method everything works fine.

    A business object with @JsonFilter:

    @JsonFilter("dataFilter")
    public class SimpleData {
    
        @JsonProperty("name")
        String firstName;
    
        @JsonProperty("secondName")
        String lastName;
    
        @JsonProperty("country")
        String country;
    
        @JsonProperty("region")
        String region;
    
        @JsonProperty("city")
        String city;
    
        @JsonProperty("genre")
        String genre;
    
        public SimpleData() {
    
            this.firstName = "Bryan";
            this.lastName = "Adams";
    
            this.country = "Canada";
            this.region = "Ontario";
            this.city = "Kingston";
            this.genre = "Rock";
    
        }
    
        public String getFirstName() {    return firstName;       }
        public void setFirstName(String firstName) {  this.firstName = firstName;  }
        public String getLastName() {   return lastName;    }
        public void setLastName(String lastName) {  this.lastName = lastName;       }
        public String getCountry() {    return country;    }
        public void setCountry(String country) {  this.country = country;      }
        public String getRegion() {   return region;     }
        public void setRegion(String region) {   this.region = region;       }
        public String getCity() {  return city;     }
        public void setCity(String city) {   this.city = city;     }
        public String getGenre() {   return genre;      }
        public void setGenre(String genre) {    this.genre = genre;     }
    }
    

    MyResource.java:

    @Path("myresource")
    public class MyResource {
    
    
        @GET
        @Produces(MediaType.APPLICATION_JSON)
        public SimpleData getIt() {
    
            return new SimpleData();
    
        }
    }
    

    A filtered result:

    enter image description here