- Notifications
You must be signed in to change notification settings - Fork 38.9k
Description
Spring Framework 5.3.15
Spring Boot 2.6.3
I set up 2 ObjectMappers (one per api version) : the last version uses the default ObjectMapper (created by Spring Boot), and i instantiate an other ObjectMapper for the version 1 (there is different settings for the dates, the null fields, and so on).
I also need to build a Jackson Filter at runtime (the filter depends on the roles of the authenticated user), for that i can use the MappingJacksonValue wrapper. But when the values are wrapped, Spring will always use the default ObjectMapper.
We can see here that the ObjectMapper is selected before unwraping the value:
Lines 195 to 211 in 4eaee1e
| @Override | |
| public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, | |
| ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) { | |
| ObjectMapper mapper = selectObjectMapper(valueType, mimeType); | |
| if (mapper == null) { | |
| throw new IllegalStateException("No ObjectMapper for " + valueType); | |
| } | |
| Class<?> jsonView = null; | |
| FilterProvider filters = null; | |
| if (value instanceof MappingJacksonValue) { | |
| MappingJacksonValue container = (MappingJacksonValue) value; | |
| value = container.getValue(); | |
| jsonView = container.getSerializationView(); | |
| filters = container.getFilters(); | |
| } | |
| ObjectWriter writer = createObjectWriter(mapper, valueType, mimeType, jsonView, hints); |
Is that "by design" or is this a missing feature?
Sample code:
@Configuration public class Config { private static final MimeType[] EMPTY_MIME_TYPES = {}; @Bean CodecCustomizer myJacksonCodecCustomizer(ObjectMapper objectMapper) { return (configurer) -> { CodecConfigurer.DefaultCodecs defaults = configurer.defaultCodecs(); defaults.jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper, EMPTY_MIME_TYPES)); Jackson2JsonEncoder jackson2JsonEncoder = new Jackson2JsonEncoder(objectMapper, EMPTY_MIME_TYPES); // API v2 will use the default object mapper jackson2JsonEncoder.registerObjectMappersForType(Controller.HelloV1.class, map -> { map.put(MediaType.APPLICATION_JSON, mapperForApiV1()); }); defaults.jackson2JsonEncoder(jackson2JsonEncoder); }; } private ObjectMapper mapperForApiV1() { Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder(); builder.featuresToEnable(SerializationFeature.WRITE_DATES_WITH_ZONE_ID); builder.serializationInclusion(JsonInclude.Include.NON_ABSENT); builder.modules(new SimpleModule(), new JavaTimeModule()); // And other settings return builder.build(); } } @RestController public class Controller { @GetMapping("/v1/hello") public Mono<HelloV1> hello1() { return Mono.just(new HelloV1("world", true, null)); } @GetMapping("/v2/hello") public Mono<HelloV2> hello2() { return Mono.just(new HelloV2("world", true, null)); } @GetMapping("/v1/wrapped-hello") public Mono<MappingJacksonValue> wrappedHello1() { MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(new HelloV1("world", true, null)); // mappingJacksonValue.setFilters(buildFilterFromRoles()); return Mono.just(mappingJacksonValue); } @GetMapping("/v2/wrapped-hello") public Mono<MappingJacksonValue> wrappedHello2() { MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(new HelloV2("world", true, null)); // mappingJacksonValue.setFilters(buildFilterFromRoles()); return Mono.just(mappingJacksonValue); } private FilterProvider buildFilterFromRoles() { // The actual filter is configured according to the roles of the authenticated user SimpleBeanPropertyFilter theFilter = SimpleBeanPropertyFilter .serializeAllExcept("canBeMasked"); return new SimpleFilterProvider().addFilter("myFilter", theFilter); } public record HelloV1(String hello, boolean canBeMasked, String nullField) {} public record HelloV2(String hi, boolean canBeMasked, String nullField) {} }Expected results:
"/v1/wrapped-hello" should return the same serialization than "/v1/hello"