23

Is it possible to selectively determine when the @JsonFilter annotation gets used at runtime?

I'm getting JsonMappingException exception (see below) when I don't provide the filter.

Background:

I learned from a recent StackOverflow post that I can use @JsonFilter to dynamically filter the bean properties getting serialized. This works great. After adding @JsonFilter("apiFilter") to my domain class and with the addition of this code in my jax-rs service (using the CXF implementation), I am able to dynamically filter the properties returned by my RESTful API:

// shortened for brevity
FilterProvider filters = new SimpleFilterProvider().addFilter("apiFilter", SimpleBeanPropertyFilter.filterOutAllExcept(filterProperties));

return mapper.filteredWriter(filters).writeValueAsString(user);

The problem is there are different service calls where I don't want to apply the filter at all. In those cases I want to return the entire domain class without filtering any properties. In the case where I just try to return the domain class I'm getting an exception as follows:

Caused by: org.codehaus.jackson.map.JsonMappingException: Can not resolve BeanPropertyFilter with id 'apiFilter'; no FilterProvider configured

at org.codehaus.jackson.map.ser.BeanSerializer.findFilter(BeanSerializer.java:252)
at org.codehaus.jackson.map.ser.BeanSerializer.serializeFieldsFiltered(BeanSerializer.java:216)
at org.codehaus.jackson.map.ser.BeanSerializer.serialize(BeanSerializer.java:140)
Community
  • 1
  • 1
Justin
  • 6,031
  • 11
  • 48
  • 82

6 Answers6

31

I know it's already been answered but for any newcommers Jackson has actually added the ability to not fail on missing filters (JACKSON-650):
You just need to call SimpleFilterProvider.setFailOnUnknownId(false) and you won't get this exception.

Ittai
  • 5,625
  • 14
  • 60
  • 97
  • 7
    Of course you still need to configure a SimpleFilterProvider even if you're not going to be using filtering at all with the mapper you're using. Oh, well. – Jules Nov 19 '13 at 22:45
20

For Spring Boot / Jackson configuration just add:

@Configuration 
public class JacksonConfiguration { 
    public JacksonConfiguration(ObjectMapper objectMapper) { 
        objectMapper.setFilterProvider(new SimpleFilterProvider().setFailOnUnknownId(false)); 
    } 
}
Pedro Bacchini
  • 876
  • 10
  • 13
13

I think you could trick the filtered writer defining an empty serialize filter for the cases where you want all the properties seralized:

FilterProvider filters = new SimpleFilterProvider().addFilter("apiFilter", SimpleBeanPropertyFilter.serializeAllExcept(emptySet));

This way, when the engine looks for the "apiFilter" filter defined at the @JsonFilter anotation, it finds it, but it will not have any effect (as will serialize all the properties).

EDIT Also, you can call the factory method writer() instead of filteredWriter():

ObjectWriter writer=null;
if(aplyFilter) {
    FilterProvider filters = new SimpleFilterProvider().addFilter("apiFilter", SimpleBeanPropertyFilter.filterOutAllExcept(filterProperties));
    writer=mapper.filteredWriter(filters);
} else {
   writer=mapper.writer();
}

return writer.writeValueAsString(user);

I think this last solution is way cleaner, and indeed better.

Tomas Narros
  • 13,390
  • 2
  • 40
  • 56
  • in your edited example, would i need to include the code to check which writer method to call in every jax-rs service call? in some service methods i return an actual User object rather than a String. many thanks for your input! – Justin Feb 21 '12 at 18:29
  • 1
    ok, i had a chance to give it a try. the "trick" you suggested works but i couldn't get your second "cleaner" suggestion to work. in that case i still receive the "no FilterProvider configured" error. thanks again. – Justin Feb 21 '12 at 22:31
  • @Justin: well, IMO an "unclean" workaround which solves a problem is better than a "clean" one not working :) . Hope it helped to solve your problem. – Tomas Narros Feb 22 '12 at 08:33
2

I had a similar issue getting the same Exception, but the accepted answer didn't really help in my case. Here's the solution that worked for me:

In my setup I was using a custom JacksonSerializer like this:

@JsonSerialize(using = MyCustomSerializer.class)
private Object someAttribute;

And that serializer was implemented like this:

public class MyCustomSerializer extends JsonSerializer<Object> {
  @Override
  public void serialize(Object o, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
    if (o != null) {
      jgen.writeObject(o);
    }
  }
}

The problem with this is, that as long as you don't use any filters, it works. It also works if you serialize primitives, so for instance if you use jgen.writeString(..). If you use filters, that code is wrong, because the filters are stored somewhere inside of the SerializerProvider, not in the JsonGenerator. If in that case you use the jsongenerator directly, a new SerializerProvider, that doesn't know about the filters, is created internally. So instead of the shorter jgen.writeObject(o) you need to call provider.defaultSerializeValue(o, jgen). That will ensure that the filters don't get lost and can be applied.

xor_eq
  • 3,933
  • 1
  • 29
  • 33
0

I have applied the same solution as mentioned accepted solution but when i am returning writer.writeValueAsString(course) as a Rest service response then i am getting response in below format

{ "status": "OK", "data": "[{\"name\":\"JPA in Use\",\"reviews\":[{\"id\":4081,\"rating\":\"4\",\"description\":\"Fine\"},{\"id\":4084,\"rating\":\"4\",\"description\":\"Ok\"}]},{\"name\":\"Spring in Use\",\"reviews\":[{\"id\":4003,\"rating\":\"3\",\"description\":\"Nice Course\"}]}]" }

But My expected Response is

{ "status": "OK", "data": [ { "name": "JPA in Use", "reviews": [ { "id": 4081, "rating": "4", "description": "Fine" }, { "id": 4082, "rating": "5", "description": "Great" } ] }, { "name": "Spring in Use", "reviews": [ { "id": 4003, "rating": "3", "description": "Nice Course" } ] } ] }

For getting my response i have applied converted the jsonstring to specfic object type

List<Course> resultcourse = mapper.readValue(writeValueAsString,List.class);

Note: Course has id,name and reviews as field and i want to suppress the id

I am providing the code snippet hope it is helpful to some.

@GetMapping("/courses")
    public ResponseEntity<JpaResponse> allCourse() throws Exception {
        JpaResponse response = null;
         ObjectMapper mapper = new ObjectMapper(); 
         mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        List<Course> course = service.findAllCourse();
        SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept("name","reviews");
        FilterProvider filterProvider = new SimpleFilterProvider().addFilter("jpafilter", filter).setFailOnUnknownId(false);
                ObjectWriter writer = mapper.writer(filterProvider);
        String writeValueAsString = writer.writeValueAsString(course);
        List<Course> resultcourse = mapper.readValue(writeValueAsString,List.class);
            response = new JpaResponse(HttpStatus.OK.name(),resultcourse);
            return new ResponseEntity<>(response, HttpStatus.OK);

}


public class JpaResponse {
        private String status;
        private Object data;
        public JpaResponse() {
            super();
        }
        public JpaResponse(String status, Object data) {
            super();
            this.status = status;
            this.data = data;
        }
}
abhinav kumar
  • 1,487
  • 1
  • 12
  • 20
0

This is what I did for Springboot, no more logic is needed to filter those fields from all the REST responses in your application, if you need to filter more POJOs just add them to the FilterProvider:

Add a configuration class with the filter:

@Configuration
public class JacksonConfiguration {

    public JacksonConfiguration(ObjectMapper objectMapper) {
        SimpleFilterProvider simpleFilterProvider = new SimpleFilterProvider();
        FilterProvider filters = simpleFilterProvider.addFilter("PojoFilterDTO",
                SimpleBeanPropertyFilter.serializeAllExcept("field1", "field2")).setFailOnUnknownId(false);
        objectMapper.setFilterProvider(filters);
        objectMapper.configure(SerializationFeature.FAIL_ON_SELF_REFERENCES, false);
    }
}

Add the JsonFilter annotation to your POJO:

@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonFilter("PojoFilterDTO")
public class PojoDTO {
}
apolo884
  • 31
  • 3