Skip to content

MappingJacksonValue and Jackson2CodecSupport#registerObjectMappersForType do not work together #28045

Closed
@ghostd

Description

@ghostd

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:

@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"

Metadata

Metadata

Assignees

Labels

in: webIssues in web modules (web, webmvc, webflux, websocket)type: bugA general bug

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions