Jackson JSON, filtering properties by path

前端 未结 2 893
闹比i
闹比i 2020-12-21 06:22

I need to filter bean properties dynamiclly on serialization.

The @JsonView isn\'t an option for me.

Assume my Bean (as Json notation):

相关标签:
2条回答
  • 2020-12-21 06:50

    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;
    }
    
    0 讨论(0)
  • 2020-12-21 07:17

    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.

    0 讨论(0)
提交回复
热议问题