7

I am trying to implement a universal method which serializes the given object to JSON, but only those properties which are passed in a collection. If possible I want to get this functionality without specifying @JsonFilter on the class. For this I am trying to use FilterExceptFilter from Jackson 2.4.1. Dependencies:

Here is what I have at the moment:

public static String serializeOnlyGivenFields(Object o,
                    Collection<String> fields) throws JsonProcessingException {
    if ((fields == null) || fields.isEmpty()) return null;

    Set<String> properties = new HashSet<String>(fields);

    SimpleBeanPropertyFilter filter =
        new SimpleBeanPropertyFilter.FilterExceptFilter(properties);
    SimpleFilterProvider fProvider = new SimpleFilterProvider();
    fProvider.addFilter("fieldFilter", filter);
    fProvider.setDefaultFilter(filter);

    ObjectMapper mapper = new ObjectMapper();
    mapper.setFilters(fProvider);

    String json = mapper.writeValueAsString(o);
    return json;
}

However, the filter is never applied. It always serializes all properties.

Set<String> fields = new HashSet<String>(); fields.add("name");
String json = Serializer.serializeOnlyGivenFields(e, fields);
System.out.println(json);

{"name":"Test entity","description":"Test description"}

I have also tried to register the FilterProvider on the ObjectWriter, but same result:

String json = mapper.writer(fProvider).writeValueAsString(o);

What am I missing? Is there a nice way to achieve this with Jackson?

Daniel Szalay
  • 4,041
  • 12
  • 57
  • 103

3 Answers3

5

Based on http://www.cowtowncoder.com/blog/archives/2011/09/entry_461.html an alternate way to set up the filter is setting up a class that extends JacksonAnnotationIntrospector and overrides findFilterId. You can then specify to find your filter in the findFilterId. This could be made to be as robust if you want based on some other map or algorithm. Below is sample code. Not sure if the performance is better than the solution above but it seems to be simpler and probably more easily extensible. I was doing this for serializing CSV using Jackson. Any feedback is welcome!

public class JSON {

private static String FILTER_NAME = "fieldFilter";

public static String serializeOnlyGivenFields(Object o,
                                              Collection<String> fields) throws JsonProcessingException {
    if ((fields == null) || fields.isEmpty()) fields = new HashSet<String>();

    Set<String> properties = new HashSet<String>(fields);

    SimpleBeanPropertyFilter filter =
            new SimpleBeanPropertyFilter.FilterExceptFilter(properties);
    SimpleFilterProvider fProvider = new SimpleFilterProvider();
    fProvider.addFilter(FILTER_NAME, filter);

    ObjectMapper mapper = new ObjectMapper();
    mapper.setAnnotationIntrospector( new AnnotationIntrospector() );

    String json = mapper.writer(fProvider).writeValueAsString(o);
    return json;
}

private static class AnnotationIntrospector extends JacksonAnnotationIntrospector {
    @Override
    public Object findFilterId(Annotated a) {
        return FILTER_NAME;
    }
}

}
Rob Baily
  • 2,770
  • 17
  • 26
0

One additional thing is that you have to indicate Java classes for which filter is to be used by @JsonFilter annotation:

@JsonFilter("fieldFilter") public class MyType { }

and then it should apply.

StaxMan
  • 113,358
  • 34
  • 211
  • 239
  • 2
    So there is no universal way of achieving this without the use of `@JsonFilter`? – Daniel Szalay Oct 22 '14 at 22:17
  • That, or `@JsonView` are probably your best bets. http://www.cowtowncoder.com/blog/archives/2011/02/entry_443.html lists common mechanisms for output filtering. – StaxMan Oct 23 '14 at 20:08
  • Oh I literally was just reading that article :) My problem with `@JsonView` is that if applied, properties that do not have a view defined get serialized anyway. This makes a hassle with a complex class hierarchy. On the other hand I do not see the point of the `@JsonFilter` annotation. I mean you apply the filter implementation on the `ObjectWriter` and then the NAME of that filter is defined as an annotation too, for erm... what exactly? – Daniel Szalay Oct 23 '14 at 20:19
  • To know which filter to apply? There may be multiple filters (for different classes); as well as most classes might not need filtering at all. Partly this is for performance; filtering is an add-on option with some overhead, and default case is one where no filtering checks or processing is done. There is no "universal filter", although if you did want something like that, you could achieve it by couple of ways (sub-classing of `AnnotationIntrospector` to claim all classes have `@JsonFilter` for example). – StaxMan Oct 23 '14 at 20:57
  • Please take a look at my answer. What do you think? – Daniel Szalay Oct 24 '14 at 08:58
  • I think the most positive thing I can say that it is an interesting way to solve the problem. I don't particularly like the solution; but if it works for you and you are content, that's what matters. The only other comment I have is that to avoid adding annotations in classes "mix-in annotations" is the usual way to go. – StaxMan Oct 27 '14 at 22:39
0

I have found a solution based on Jackson: How to add custom property to the JSON without modifying the POJO. I override BeanSerializer#serializeFields to always use BeanSerializer#serializeFieldsFiltered instead. This way the filter is always applied.

Performance-wise not a very good solution, since an ObjectMapper has to be constructed at every method call. Feel free to post improvements or suggestions!

Module implementation:

public class FilteredModule extends SimpleModule {
    private static final long serialVersionUID = 1L;

    @Override
    public void setupModule(SetupContext context) {
        super.setupModule(context);

        context.addBeanSerializerModifier(new BeanSerializerModifier() {

            @Override
            public JsonSerializer<?> modifySerializer(
                    SerializationConfig config,
                    BeanDescription beanDesc,
                    JsonSerializer<?> serializer) {
                if (serializer instanceof BeanSerializerBase) { 
                    return new FilteredBeanSerializer(
                            (BeanSerializerBase) serializer);
                } 
                return serializer; 

            }                   
        });
    }

    private class FilteredBeanSerializer extends BeanSerializer {

        public FilteredBeanSerializer(BeanSerializerBase source) {
            super(source);
        }

        @Override
        protected void serializeFields(Object arg0, JsonGenerator arg1,
                SerializerProvider arg2) throws IOException,
                JsonGenerationException {
            super.serializeFieldsFiltered(arg0, arg1, arg2);
        }

    }
}

API method:

public static String serializeOnlyGivenFields(Object o,
            Collection<String> fields) throws JsonProcessingException {
    if ((fields == null) || fields.isEmpty()) fields = new HashSet<String>();

    Set<String> properties = new HashSet<String>(fields);

    SimpleBeanPropertyFilter filter =
            new SimpleBeanPropertyFilter.FilterExceptFilter(properties);
    SimpleFilterProvider fProvider = new SimpleFilterProvider();
    fProvider.addFilter("fieldFilter", filter);
    fProvider.setDefaultFilter(filter);

    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(new FilteredModule());

    String json = mapper.writer(fProvider).writeValueAsString(o);
    return json;
}

Example

Entity e = new Entity("Test entity", "Test description");   
Set<String> fields = new HashSet<String>(); fields.add("name");
String json = JSON.serializeOnlyGivenFields(e, fields);
System.out.println(json);

{"name":"Test entity"}

Benchmark: 1000 iterations on the same object

serializeOnlyGivenFields:           536 ms
serialize (reuses ObjectMapper):    23 ms
Community
  • 1
  • 1
Daniel Szalay
  • 4,041
  • 12
  • 57
  • 103