I need to exclude certain fields based on the permissions of the caller. For example, an employee profile may contain a taxpayer identifier, which is considered confidential information and should only be serialized if the caller is a member of the Payrole department. Since I use Spring Security, I want to integrate Jackson with the current security context.
public class EmployeeProfile { private String givenName; private String surname; private String emailAddress; @VisibleWhen("hasRole('PayroleSpecialist')") private String taxpayerId; }
The most obvious way to do this is through Jackson's filtering mechanism, but it has several limitations:
- Jackson does not support nested filters, so adding an access filter prohibits the use of filters for any other purpose.
- Jackson annotations cannot be added to existing, third-party classes.
- Jackson filters are not for the general type. The goal is to write a custom filter for each class that you want to apply for filtering. For example, I need to filter classes A and B, then you need to write AFilter and BFilter.
For my use case, the solution is to use a specialized annotation introspector in combination with a chain filter.
public class VisibilityAnnotationIntrospector extends JacksonAnnotationIntrospector { private static final long serialVersionUID = 1L; @Override public Object findFilterId(Annotated a) { Object result = super.findFilterId(a); if (null != result) return result;
This is basically an instance of SimpleBeanProvider, which replaces calls to include with calls to isVisible . I will probably update this to use Java 8 BiPredicate to make the solution more general, but works for now. This class also takes another filter as an argument and will give it the final decision on whether to serialize the field if the field is visible.
public class AuthorizationFilter extends SimpleBeanPropertyFilter { private final PropertyFilter antecedent; public AuthorizationFilter() { this(null); } public AuthorizationFilter(final PropertyFilter filter) { this.antecedent = null != filter ? filter : serializeAll(); } @Deprecated @Override public void serializeAsField(Object bean, JsonGenerator jgen, SerializerProvider provider, BeanPropertyWriter writer) throws Exception { if (isVisible(bean, writer)) { this.antecedent.serializeAsField(bean, jgen, provider, writer); } else if (!jgen.canOmitFields()) {
Then I add a custom filter provider for each instance of ObjectMapper.
@SuppressWarnings("deprecation") public class VisibilityFilterProvider extends SimpleFilterProvider { private static final long serialVersionUID = 1L; static final String FILTER_ID = "dummy-filter-id"; @Override public BeanPropertyFilter findFilter(Object filterId) { return super.findFilter(filterId); } @Override public PropertyFilter findPropertyFilter(Object filterId, Object valueToFilter) { if (FILTER_ID.equals(filterId)) {
Finally, I have a Jackson module that automatically registers my own annotation introspector, so I donβt need to add it to each instance of ObjectMapper manually.
public class FieldVisibilityModule extends SimpleModule { private static final long serialVersionUID = 1L; public FieldVisibilityModule() { super(PackageVersion.VERSION); } @Override public void setupModule(Module.SetupContext context) { super.setupModule(context);
There are more improvements that can be made, and I still have more unit tests for writing (e.g. handling arrays and collections), but this is the main strategy I used.