Saturday, May 24, 2014

How to implement Jackson filters to filter the REST JSON response

Background

We are building an application that supports REST calls to our API. We use JBoss AS 7.1.1 and its default framework 'RestEasy' and Jackson v1.9 JSON processor. Since it is a REST application, we may need to provide provision for the clients to filter the JSON response. As other RESTFul service application does, we planned to have a query parameter called "fields" to do this. For instance, the URL that the client has to submit will be,https:///rest/product/123?fields=id,name

The above GET request URL should return the JSON object of type "product" matching id=123. But note that the response JSON object should contain only the "id" and "name" fields in it as follows: {"id": "123", "name":"xyz"}

Implementation

To do this, we may need to fetch the value of query parameter "fields" from HttpRequest. This could be achieved using javax.ws.rs annotation @QueryParam as given below.


@GET
@Produces (MediaType. APPLICATION_JSON) 
@Path( "/{id:" + ID_PATTERN + "}" ) 
public String getProduct(@PathParam ("id" ) String id, 
        @QueryParam ("fields" ) String fields) { 
    String productJson = null ; 
    final ProductModel model = productService.read(id);         
    try { 
        productJson = filterJsonFields(model, ProductModel.class.getName(), fields); 
    } catch (IOException e) { 
        throw new ApplicationRuntimeException( 
                e.getCause() + ": " + e.getLocalizedMessage()) 
                .setWebStatus(Status. INTERNAL_SERVER_ERROR ); 
    } 
  return productJson ; 
}


After fetching the whole Product object from the database, we could filter the fields of the object while serializing the Java object to JSON object, which will be returned as the response. This filtering mechanism can be done by many ways using Jackson processor. But I know two ways of doing this.
  1. Using @JsonFilter
  2. Overiding the JacksonAnnotationIntrospector

1. Using @JsonFilter

This is the simple way to filter the response object. For this, we need to declare the @JsonFilter annotation on top of our model bean classes. 

@JsonFilter(com.test.ProductModel)  
public class ProductModel implements IModel {  
    private String id ;  
    private String name ;  
    ....  
    ....  
    ...getter and setter methods...
}


Filtering mechanism

 public static String filterJsonFields(IModel model,  
  String jsonFilter, String fieldsQueryParam) throws IOException { 
          
    String jsonResult = null ;  
    SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter.  
                     serializeAllExcept(Collections.<String> emptySet());  
    Set<String> filterProps = new HashSet<String>();      
    if (fieldsQueryParam != null) {  
        StringTokenizer st = new StringTokenizer(fieldsQueryParam, "," );  
        while (st.hasMoreTokens()) {  
            filterProps.add(st.nextToken());  
        }  
        filter = SimpleBeanPropertyFilter.filterOutAllExcept(filterProps);  
    }         
    ObjectMapper mapper = new ObjectMapper();  
    FilterProvider fProvider = new SimpleFilterProvider().addFilter(jsonFilter, filter)  
                        // setFailOnUnknownId: Ignore filtering the reference member fields  
                        // Refer: https://jira.codehaus.org/browse/JACKSON-650  
                        .setFailOnUnknownId( false );  
    try {  
        jsonResult = mapper.writer(fProvider).writeValueAsString(model);  
    } catch (IOException e) {  
        throw e;  
    }  
    return jsonResult;  
}   
   

The "jsonFilter" method parameter is the class name of the model bean that is passed from the caller method (getProduct()).

Caveat: But this didn't work well with JBoss's RestEasy.


2. Overriding the JacksonAnnotationIntrospector

In this method, we need to extend the JacksonAnnotationIntrospector and override the method "findFilterId". During serialization, Jackson calls the JacksonAnnotationIntrospector.findFilterId() to identify the filter of the model bean. In our case, we are using the name of the model bean class as its filter name. 

import org.codehaus.jackson.map.introspect.AnnotatedClass;  
import org.codehaus.jackson.map.introspect.JacksonAnnotationIntrospector;  
   
public class MyFilteringIntrospector extends JacksonAnnotationIntrospector {  
       
    @Override  
    public Object findFilterId(AnnotatedClass ac) {  
        Object id = super .findFilterId(ac); 
        if (id == null ) {  
            // the name of the class will be used as its filter name  
            id = ac.getName();  
        }  
        return id;  
    }  
} 

We may need to tell Jackson to use our custom AnnotationIntrospector and not JacksonAnnotationIntrospector. For this, 

AnnotationIntrospector myFilterIntrospector = new MyFilteringIntrospector();
mapper.setAnnotationIntrospector(myFilterIntrospector );

Hence the filterJsonFields() method is updated with the above lines of code.


Filtering mechanism

 public static String filterJsonFields(IModel model,  
           String jsonFilter, String fieldsQueryParam) throws IOException {  
   
    String jsonResult = null ;  
    SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter.  
                    serializeAllExcept(Collections.<String> emptySet());  
    Set<String> filterProps = new HashSet<String>();      
    if (fieldsQueryParam != null) {  
        StringTokenizer st = new StringTokenizer(fieldsQueryParam, ",");  
        while (st.hasMoreTokens()) {  
            filterProps.add(st.nextToken());  
        }  
        filter = SimpleBeanPropertyFilter.filterOutAllExcept(filterProps);  
    }       
    ObjectMapper mapper = new ObjectMapper();  
    AnnotationIntrospector myFilterIntrospector = new MyFilteringIntrospector();  
    mapper.setAnnotationIntrospector(myFilterIntrospector);  
    FilterProvider fProvider = new SimpleFilterProvider().addFilter(jsonFilter, filter)  
                  // setFailOnUnknownId: Ignore filtering the reference member fields  
                  // Refer: https://jira.codehaus.org/browse/JACKSON-650  
                  .setFailOnUnknownId( false );  
    try {  
        jsonResult = mapper.writer(fProvider).writeValueAsString(model);  
    } catch (IOException e) {  
        throw e;  
    }  
    return jsonResult;  
}   

1 comment:

Anushree said...

I want to make URL query parameters case insensitive and still do the above. Casing of the key in JSON will be as same as in pojo. How to do this?