Jackson @JsonFilter not applicable when used at field or method level

I am using Spring version 4.3.3 and Jackson version 2.8.3. I am trying to filter certain fields from a bean based on some user logic that is determined at runtime. @JsonFilter seems ideal for this type of functionality. The problem is that when I put it at the field or method level, my custom filter will never be called. If I put it at the class level, it will be called just fine. I do not want to use it at the class level, but since then I will need to separately maintain a list of hard-coded field names to which I want to apply logic. As with Jackson 2.3, it is assumed that the possibility of placing this annotation at the field level will exist.

Here is the simplest user filter without any user logic:

public class MyFilter extends SimpleBeanPropertyFilter { @Override protected boolean include(BeanPropertyWriter beanPropertyWriter) { return true; } @Override protected boolean include(PropertyWriter propertyWriter) { return true; } 

}

Then I have the Jackson ObjectMapper configuration:

 public class MyObjectMapper extends ObjectMapper { public MyObjectMapper () { SimpleFilterProvider filterProvider = new SimpleFilterProvider(); filterProvider.addFilter("myFilter", new MyFilter()); setFilterProvider(filterProvider); } } 

Then finally, I have a bean entity:

 @Entity public class Project implements Serializable { private Long id; private Long version; @JsonFilter("myFilter") private String name; @JsonFilter("myFilter") private String description; // getters and setters } 

If I move the @JsonFilter annotation to the class level, where @Entity, the filter is at least called, but when it is at the field level, as in the example here, it is never called.

+7
source share
5 answers

I have the same need, but after studying unit tests, I found that this is not a use case related to annotating a field.

Annotating a field causes a filter by the value of the field, not the instance containing the field. For example, imagine that you must have classes A and B, where A contains a field of type B.

 class A { @JsonFilter("myFilter") B foo; } 

Jackson applies "myFilter" to fields B not in A. Since your example contains fields of type String, which has no fields, Jackson never calls your filter.

+4
source

You can try this approach for the same purpose:

 @Entity @Inheritance( strategy = InheritanceType.SINGLE_TABLE ) @DiscriminatorColumn( discriminatorType = DiscriminatorType.STRING, length = 2 ) @Table( name = "project" ) @JsonTypeInfo( use = Id.CLASS, include = As.PROPERTY, property = "@class" ) @JsonSubTypes({ @Type( value = BasicProject.class, name = "basicProject" ), @Type( value = AdvanceProject.class, name = "advanceProject" )}) public abstract class Project { private Long id; private Long version; } @Entity @DiscriminatorValue("AD") public class AdvanceProject extends Project { private String name; private String description; } @Entity @DiscriminatorValue("BS") public class BasicProject extends Project { private String name; } 
0
source

I do not think that you will earn. I tried and these are the results of my investigation, perhaps it will be useful.

First of all, as @Faron noted, the @JsonFilter annotation @JsonFilter applied to the annotated class, not the field.

Secondly, as I understand it. Imagine, somewhere in Jackson’s interior you can get the actual field. You can find out if there is an annotation using the Java Reflection API. You can even get the filter name. Then you get into the filter and pass the value of the field. But this happens at runtime, how do you get the appropriate JsonSerializer type JsonSerializer if you decide to serialize the field? This is not possible due to erasing styles.

The only alternative I see is to forget about dynamic logic. Then you can do the following:

1) JacksonAnnotationIntrospector extension (almost the same as the AnnotationIntrospector implementation, but not useless default code), overriding the hasIgnoreMarker method. See this answer

2) the offender begins here. An invisible way based on your original goal, but still: extend the BeanSerializerModifier and filter the fields there. An example can be found here . That way, you can define a serializer that doesn't actually serialize anything (again, I understand how strange this is, but maybe it will be useful)

3), similar to the approach described above: define a useless BeanDescription based BeanDescription that implements the ContextualSerializer createContextual method. An example of this magic here

0
source

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; // By always returning a value, we cause Jackson to query the filter provider. // A more sophisticated solution will introspect the annotated class and only // return a value if the class contains annotated properties. return a instanceof AnnotatedClass ? VisibilityFilterProvider.FILTER_ID : null; } } 

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()) { // since 2.3 writer.serializeAsOmittedField(bean, jgen, provider); } } @Override public void serializeAsField(Object pojo, JsonGenerator jgen, SerializerProvider provider, PropertyWriter writer) throws Exception { if (isVisible(pojo, writer)) { this.antecedent.serializeAsField(pojo, jgen, provider, writer); } else if (!jgen.canOmitFields()) { // since 2.3 writer.serializeAsOmittedField(pojo, jgen, provider); } } @Override public void serializeAsElement(Object elementValue, JsonGenerator jgen, SerializerProvider provider, PropertyWriter writer) throws Exception { if (isVisible(elementValue, writer)) { this.antecedent.serializeAsElement(elementValue, jgen, provider, writer); } } private static boolean isVisible(Object pojo, PropertyWriter writer) { // Code to determine if the field should be serialized. } } 

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)) { // This implies that the class did not have an explict filter annotation. return new AuthorizationFilter(null); } // The class has an explicit filter annotation so delegate to it. final PropertyFilter antecedent = super.findPropertyFilter(filterId, valueToFilter); return new VisibilityPropertyFilter(antecedent); } } 

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); // Append after other introspectors (instead of before) since // explicit annotations should have precedence context.appendAnnotationIntrospector(new VisibilityAnnotationIntrospector()); } } 

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.

0
source

Thanks to this really good blog, I was able to use @JsonView to filter out certain fields from an entity bean based on some custom logic defined at runtime.

Since @JsonFilter does not apply to fields in the class, I found this to be a cleaner workaround.

Here is a sample code:

 @Data @AllArgsConstructor public class TestEntity { private String a; @JsonView(CustomerLogViews.SecureAccess.class) private Date b; @JsonView(CustomerLogViews.SecureAccess.class) private Integer c; private List<String> d; } public class CustomerLogViews { public static interface GeneralAccess {} public static interface SecureAccess {} public static class GeneralAccessClass implements GeneralAccess {} public static class SecureAccessClass implements SecureAccess, GeneralAccess {} public static Class getWriterView(final boolean hasSecureAccess) { return hasSecureAccess ? SecureAccessClass.class : GeneralAccessClass.class; } } @Test public void test() throws JsonProcessingException { final boolean hasSecureAccess = false; // Custom logic resolved to a boolean value at runtime. final TestEntity testEntity = new TestEntity("1", new Date(), 2, ImmutableList.of("3", "4", "5")); final ObjectMapper objectMapper = new ObjectMapper().enable(MapperFeature.DEFAULT_VIEW_INCLUSION); final String serializedValue = objectMapper .writerWithView(CustomerLogViews.getWriterView(hasSecureAccess)) .writeValueAsString(testEntity); Assert.assertTrue(serializedValue.contains("a")); Assert.assertFalse(serializedValue.contains("b")); Assert.assertFalse(serializedValue.contains("c")); Assert.assertTrue(serializedValue.contains("d")); } 
0
source

Source: https://habr.com/ru/post/1257450/


All Articles