@JsonView
annotation declared on a controller method. But I don't think Jackson's views are good for that. First of all you have to annotate methods of the class itself with @JsonView
to filter its properties out. Then you have to define additional classes for the views. I think it'd be much easier just to list all the required properties for a controller method itself. So I added custom annotations for Spring MVC controllers:
/** * Annotation applied to a Spring MVC {@code @RequestMapping} or {@code @ExceptionHandler} method to filter out * its response body during JSON serialization. * <p/> * Example: * <pre> * public class PropSet extends PredefinedPropSet { * public PropSet() { * super("propA"); * } * } * * @JsonFilter(target = Bean.class, include = {"propB"}, propSets = PropSet.class) * public @RequestBody getBean() { * ... * } * </pre> * * @author Oleg Galkin * @see com.fasterxml.jackson.databind.ObjectMapper#setFilterProvider(com.fasterxml.jackson.databind.ser.FilterProvider) * @see com.fasterxml.jackson.databind.ObjectMapper#setAnnotationIntrospector(com.fasterxml.jackson.databind.AnnotationIntrospector) */ @Documented @Target(METHOD) @Retention(RUNTIME) public @interface JsonFilter { /** * Class the filter is used for. */ Class<?> target(); /** * Explicitly defined properties that should be kept when serializing the specified class to JSON. */ String[] include() default {}; /** * {@link PredefinedPropSet Property sets} that define the properties included in JSON. * Each property set class must have a default constructor. */ Class<? extends PredefinedPropSet>[] propSets() default {}; } /** * Declares several {@link JsonFilter} annotations for different classes on the same method. * * @author Oleg Galkin */ @Documented @Target(METHOD) @Retention(RUNTIME) public @interface JsonFilters { /** * Defined filters. */ JsonFilter[] value() default {}; }The annotations can be used as follows:
@JsonFilters({ @JsonFilter(target = User.class, propSets = {IdPropSet.class, UserPropSet.class}), @JsonFilter(target = Role.class, include = {"id", "name"}) }) @RequestMapping(value = "/user/{id}", method = RequestMethod.GET) @ResponseBody public User getUser(long id) { ... }They allow listing properties included in JSON explicitly or using special property sets inherited from
PredefinedPropSet
.To remove unnecessary properties we need Jackson's filter that only serializes the allowed object properties
public class ExceptPropertyFilter implements PropertyFilter { private final Map<Class<?>, Set<String>> filters = Maps.newHashMap(); ... @Override public void serializeAsField(Object pojo, JsonGenerator jgen, SerializerProvider prov, PropertyWriter writer) throws Exception { if (include(pojo, writer)) { writer.serializeAsField(pojo, jgen, prov); } else if (!jgen.canOmitFields()) { writer.serializeAsOmittedField(pojo, jgen, prov); } } ... protected boolean include(Object object, PropertyWriter writer) { Validate.notNull(object); Set<String> properties = getProperties(object); return properties == null || properties.contains(writer.getName()); } private Set<String> getProperties(Object object) { for (Class<?> cls = object.getClass(); cls != Object.class; cls = cls.getSuperclass()) { Set<String> fields = filters.get(cls); if (fields != null) { return fields; } } return null; } }The filter must be defined for all classes. Because Jackson uses string filter identifiers to find a specific filter, we have to define one and get it known to Jackson. To do it we can override the annotation introspector Jackson has:
public class JsonFilterAnnotationIntrospector extends JacksonAnnotationIntrospector { private final String filterId; ,,, @Override public Object findFilterId(Annotated ann) { Object id = super.findFilterId(ann); if (id == null) { JavaType javaType = TypeFactory.defaultInstance().constructType(ann.getRawType()); if (!javaType.isContainerType()) { id = filterId; } } return id; } }The annotation introspector must be set to the object mapper Spring MVC
MappingJackson2HttpMessageConverter
will use. It can be done by overriding a WebMvcConfigurerAdapter
method:
@Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json() .annotationIntrospector(new JsonFilterAnnotationIntrospector()) .filters(new SimpleFilterProvider().setFailOnUnknownId(false)) .build(); converters.add(new MappingJackson2HttpMessageConverter(objectMapper)); }And finally we need to activate the filter when a method annotated with
@JsonFilter
is invoked by implementing Spring MVC ResponseBodyAdvice
interface:
/** * {@link ResponseBodyAdvice} implementation that supports the {@link JsonFilter} annotation declared on a Spring MVC * {@code @RequestMapping} or {@code @ExceptionHandler} method. * <p/> * The created {@link ExceptPropertyFilter} is used within {@link MappingJackson2HttpMessageConverter} to serialize * the response body to JSON. * * @author Oleg Galkin * @see JsonFilter * @see ExceptPropertyFilter * @see MappingJackson2HttpMessageConverter */ @ControllerAdvice public class JsonFilterResponseBodyAdvice extends AbstractMappingJacksonResponseBodyAdvice { @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return super.supports(returnType, converterType) && (returnType.getMethodAnnotation(JsonFilter.class) != null || returnType.getMethodAnnotation(JsonFilters.class) != null); } @Override protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType, MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) { ExceptPropertyFilter filter; JsonFilter annotation = returnType.getMethodAnnotation(JsonFilter.class); if (annotation != null) { filter = new ExceptPropertyFilter(annotation); } else { filter = new ExceptPropertyFilter(returnType.getMethodAnnotation(JsonFilters.class).value()); } SimpleFilterProvider filters = new SimpleFilterProvider(); filters.addFilter(JsonFilterAnnotationIntrospector.DEFAULT_FILTER_ID, filter); bodyContainer.setFilters(filters); } }The code is available here.