I need to filter bean properties dynamiclly on serialization.
The @JsonView
isn\'t an option for me.
Assume my Bean (as Json notation):
There's Jackson plugin called squiggly for doing exactly this.
String filter = "name,children[childName]";
ObjectMapper mapper = Squiggly.init(this.objectMapper, filter);
mapper.writeValue(response.getOutputStream(), myBean);
You could integrate it into a MessageConverter
or similar, driven by annotations, as you see fit.
If you have a fixed number of possible options, then there is a static solution too: @JsonView
public interface NameAndChildName {}
@JsonView(NameAndChildName.class)
@ResponseBody
public List<Entity> findAll() {
return serviceEntity.findAll();
}
public class Entity {
public String id;
@JsonView(NameAndChildName.class)
public String name;
@JsonView({NameAndChildName.class, SomeOtherView.class})
public List<Child> children;
}
public class Child {
@JsonView(SomeOtherView.class)
public String id;
@JsonView(NameAndChildName.class)
public String childName;
}
Well, it's tricky but doable. You can do this using Jacksons Filter
feature (http://wiki.fasterxml.com/JacksonFeatureJsonFilter) with some minor alterations. To start, we are going to use class name for filter id, this way you won't have to add @JsonFIlter
to every entity you use:
public class CustomIntrospector extends JacksonAnnotationIntrospector {
@Override
public Object findFilterId(AnnotatedClass ac) {
return ac.getRawType();
}
}
Next step, make that filter of super class will apply to all of its subclasses:
public class CustomFilterProvider extends SimpleFilterProvider {
@Override
public BeanPropertyFilter findFilter(Object filterId) {
Class id = (Class) filterId;
BeanPropertyFilter f = null;
while (id != Object.class && f == null) {
f = _filtersById.get(id.getName());
id = id.getSuperclass();
}
// Part from superclass
if (f == null) {
f = _defaultFilter;
if (f == null && _cfgFailOnUnknownId) {
throw new IllegalArgumentException("No filter configured with id '" + filterId + "' (type " + filterId.getClass().getName() + ")");
}
}
return f;
}
}
Custom version of ObjectMapper
that utilizes our custom classes:
public class JsonObjectMapper extends ObjectMapper {
CustomFilterProvider filters;
public JsonObjectMapper() {
filters = new CustomFilterProvider();
filters.setFailOnUnknownId(false);
this.setFilters(this.filters);
this.setAnnotationIntrospector(new CustomIntrospector());
}
/* You can change methods below as you see fit. */
public JsonObjectMapper addFilterAllExceptFilter(Class clazz, String... property) {
filters.addFilter(clazz.getName(), SimpleBeanPropertyFilter.filterOutAllExcept(property));
return this;
}
public JsonObjectMapper addSerializeAllExceptFilter(Class clazz, String... property) {
filters.addFilter(clazz.getName(), SimpleBeanPropertyFilter.serializeAllExcept(property));
return this;
}
}
Now take a look at MappingJackson2HttpMessageConverter
, you will see that it uses one instane of ObjectMapper
internaly, ergo you cannot use it if you want different configurations simultaneously (for different requests). You need request scoped ObjectMapper
and appropriate message converter that uses it:
public abstract class DynamicMappingJacksonHttpMessageConverter extends MappingJackson2HttpMessageConverter {
// Spring will override this method with one that provides request scoped bean
@Override
public abstract ObjectMapper getObjectMapper();
@Override
public void setObjectMapper(ObjectMapper objectMapper) {
// We dont need that anymore
}
/* Additionally, you need to override all methods that use objectMapper attribute and change them to use getObjectMapper() method instead */
}
Add some bean definitions:
<bean id="jsonObjectMapper" class="your.package.name.JsonObjectMapper" scope="request">
<aop:scoped-proxy/>
</bean>
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="your.package.name.DynamicMappingJacksonHttpMessageConverter">
<lookup-method name="getObjectMapper" bean="jsonObjectMapper"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
And the last part is to implement something that will detect your annotation and perform actual configuration. For that you can create an @Aspect
. Something like:
@Aspect
public class JsonResponseConfigurationAspect {
@Autowired
private JsonObjectMapper objectMapper;
@Around("@annotation(jsonFilterProperties)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
/* Here you will have to determine return type and annotation value from jointPoint object. */
/* See http://stackoverflow.com/questions/2559255/spring-aop-how-to-get-the-annotations-of-the-adviced-method for more info */
/* If you want to use things like 'children.childName' you will have to use reflection to determine 'children' type, and so on. */
}
}
Personally, I use this in a different way. I dont use annotations and just do configuration manually:
@Autowired
private JsonObjectMapper objectMapper;
@RequestMapping("/rest/entity")
@ResponseBody
public List<Entity> findAll() {
objectMapper.addFilterAllExceptFilter(Entity.class, "name", "children");
objectMapper.addFilterAllExceptFilter(EntityChildren.class, "childName");
return serviceEntity.findAll();
}
P.S. This approach has one major flaw: you cannot add two different filters for one class.