Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MappingJacksonValue and Jackson2CodecSupport#registerObjectMappersForType do not work together #28045

Closed
ghostd opened this issue Feb 12, 2022 · 1 comment
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: bug A general bug
Milestone

Comments

@ghostd
Copy link

ghostd commented Feb 12, 2022

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"

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Feb 12, 2022
@ghostd ghostd changed the title MappingJacksonValue and Jackson2CodecSupport#registerObjectMappersForType does not work together MappingJacksonValue and Jackson2CodecSupport#registerObjectMappersForType do not work together Feb 13, 2022
@rstoyanchev rstoyanchev self-assigned this Feb 25, 2022
@rstoyanchev rstoyanchev added in: web Issues in web modules (web, webmvc, webflux, websocket) type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Feb 25, 2022
@rstoyanchev rstoyanchev added this to the 5.3.17 milestone Feb 25, 2022
@rstoyanchev
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: bug A general bug
Projects
None yet
Development

No branches or pull requests

3 participants