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: https://github.com/spring-projects/spring-framework/blob/4eaee1e7381d5f3d8cd6e3ab77c8cfcf7ef2d716/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java#L195-L211

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"

Comment From: rstoyanchev

Indeed selectObjectMapper should ignore the MappingJacksonValue wrapper. Before selectObjectMapper was introduced we haven't had to consider the wrapper, e.g. in canEncode. It just hasn't come up as an issue but arguably that should also be checking against the value type.