Advanced filtering with Jackson, Json Filters
I wrote a bit earlier on "filtering properties with Jackson". While it was comprehensive in that all main methods of filtering were covered, there wasn't much depth. Specifically, only very basic usage of Json Filters (@JsonFilter annotation, SimpleFilterProvider as provider) was considered. This approach does allow more dynamic filtering than, say, @JsonView, but it is still somewhat limited. So let's consider more advanced customizability.
1. Refresher on Json Filters
Ok, so the basic idea with Json Filters is that:
- Classes can have an associated Filter Id, which defines logical filter to use.
- A provider is needed to get the actual filter instance to use, given id: this will be configured by assigning a FilterProvider (such as 'SimpleFilterProvider') to ObjectMapper or ObjectWriter.
- Jackson will dynamically (and efficiently) resolve filter given class uses, dynamically, allowing per-call reconfiguration of filtering.
From this it is clear that there are 2 main things you can configure: mechanism that is used to find Filter id of a given class, and mechanism used for mapping this id to actual filter used (implementation of which can be as complicated as you want).
So let's have a look at both parts.
2. Configuring mapping from id to filter instance
Of mechanisms, latter one may be easier to understand and use: one just has to implement 'FilterProvider', which has but one method to implement:
public abstract class FilterProvider { public abstract BeanPropertyFilter findFilter(Object filterId); }
given this, 'SimpleFilterProvider' is little more than a Map<String,BeanPropertyFilter>, except for adding couple of convenience factory methods that build 'SimpleBeanPropertyFilter' instances given property names, so you typically just instantiate one with calls like:
SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept("a"));
which would out all properties except for one named "a". This filter is then configured with ObjectMapper like so:
FilterProvider fp = new SimpleFilterProvider().addFilter("onlyAFilter", filter); objectMapper.writer(fp).writeValueAsString(pojo);
which would, then, apply to any Java type configured to use filter with id "onlyAFilter".
3. Configuring discovery of filter id
From above example we know we need to indicate classes that are to use our "onlyAFilter". The default mechanism is to use:
@JsonFilter("onlyAFilter") public class FilteredPOJO { //... }
But this is just the default. How so? The way Jackson figures out its annotation-based configuration is actually indirect, and fully customizable: all interaction is through configured 'AnnotationIntrospector' object, which amongst other things defines this method:
public Object findFilterId(AnnotatedClass ac);
which is called when serializer needs to determine id of the filter to
apply (if any) for given class. Since the default implementation
(org.codehaus.jackson.map.introspect.JacksonAnnotationIntrospector) has
everything else working fine, what we can do is to sub-class it and
override this method.
For example:
public class MyFilteringIntrospector extends JacksonAnnotationIntrospector { @Override public Object findFilterId(AnnotatedClass ac) { // First, let's consider @JsonFilter by calling superclass Object id = super.findFilterId(ac); // but if not found, use our own heuristic; say, just use class name as filter id, if there's "Filter" in name: if (id == null) { String name = ac.getName(); if (name.indexOf("Filter") >= 0) { id = name; } } return id; } }
Above functionality is just to show what is possible, not that it makes sense. Alternatively you could of course define your own annotations to check; or have List of known class names, check class definition or interfaces type implements. The main point is just that you are not limited to using @JsonFilter annotation, but can use pretty much any logic you want, within limits of your coding skills.
The only caveat is that the resolution from Class to matching id is only guaranteed to be called once per ObjectMapper; so any variation in filtering of specific class needs to happen at either mapping of id to filter, or within filter itself.
4. Don't be afraid of sub-classing (Jackson)AnnotationIntrospector
Actually, the key take away might as well be the fact that AnnotationIntrospector is designed to be customizable. It was initially created to allow easy reuse of JAXB annotations (via JAXBAnnotationIntrospector; combining things with AnnotationIntrospector.Pair); but it is also a very powerful general-purpose customization mechanism. But at this point quite underused one at that.
5. Addendum
Some additional notes based on feedback I received:
- Custom BeanPropertyFilter implementations are obviously powerful too: not only can they completely change what (if anything) gets written for property, they can base this on all configuration accessible via SerializerProvider which is passed to serializeAsField(): for example, it can check to see what serialization view is available by calling 'provider.getSerializationView()'.