FluxMappingJacksonValue.zip Affects: \5.3.3
When trying to filter Rest Controller Flux output using MappingJacksonValue, you receive an exception instead of filtered JSON.
org.springframework.core.codec.CodecException: Type definition error: [simple type, class com.example.Example]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot resolve PropertyFilter with id 'columns'; no FilterProvider configured (through reference chain: java.util.ArrayList[0]->org.springframework.http.converter.json.MappingJacksonValue["value"])
If you collect the Flux to Mono, and apply MappingJacksonValue to it - it works as expected.
Minimal project for reproducing the issue, with tests, is attached.
Comment From: poutsma
When marshaling non-streaming JSON, Spring WebFlux will convert a Flux<T>
into a Mono<List<T>>
first, and then marshals that list as a whole in order to be more efficient.
@GetMapping("/example-flux")
public Flux<MappingJacksonValue> exampleFlux() {
SimpleBeanPropertyFilter propertyFilter = SimpleBeanPropertyFilter.filterOutAllExcept("foo");
FilterProvider filters = new SimpleFilterProvider().addFilter("columns", propertyFilter);
return Flux.just(new Example("foo", "bar"))
.map(MappingJacksonValue::new)
.doOnNext(value -> value.setFilters(filters))
;
}
In the exampleFlux
method of your sample, this means that the flux of MappingJacksonValue
objects are turned in a list, and passed on to Jackson. Jackson then tries to find the property filter 'columns', but can't find it, because that filter is configured on the MappingJacksonValue
objects in the list; not the list itself.
@GetMapping("/example-mono")
public Mono<MappingJacksonValue> exampleMono() {
SimpleBeanPropertyFilter propertyFilter = SimpleBeanPropertyFilter.filterOutAllExcept("foo");
FilterProvider filters = new SimpleFilterProvider().addFilter("columns", propertyFilter);
return Flux.just(new Example("foo", "bar"))
.collectList()
.map(MappingJacksonValue::new)
.doOnNext(value -> value.setFilters(filters))
;
}
The exampleMono
method essentially does the same thing, but in a different order, which does allow WebFlux to find the filter.
I am not entirely sure if and how MappingJacksonValue
is supposed to be used with Flux
instances (or any multi-value publisher really). I could consider something like the following:
@GetMapping("/example-mapping-value")
public MappingJacksonValue exampleMappingValue() {
SimpleBeanPropertyFilter propertyFilter = SimpleBeanPropertyFilter.filterOutAllExcept("foo");
FilterProvider filters = new SimpleFilterProvider().addFilter("columns", propertyFilter);
Flux<Example> flux = Flux.just(new Example("foo", "bar"));
MappingJacksonValue value = new MappingJacksonValue(flux);
value.setFilters(filters);
return value;
}
but this is currently not supported and would non-trivial to add. Do you have any thoughts, @rstoyanchev ?
Comment From: poutsma
After some discussion, we have concluded that neither Flux<MappingJacksonValue<Example>>
nor MappingJacksonValue<Flux<Example>>
are supported as return value from a controller. The former would not be a good model to begin with, since the wrapper applies to the whole series. For the latter, there is a good workaround in the form of your exampleMono
method.
Comment From: kzander91
@poutsma I was wondering if we could revisit this issue with Spring Framework 6, since the AbstractJackson2Encoder
no longer uses collectList()
when encoding a Flux
and serializes each item individually, even in non-streaming cases:
https://github.com/spring-projects/spring-framework/blob/0abadaa2f65479e77a4ab4201c620c50f9118950/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java#L156-L215
In my application, I need MappingJacksonValue
and am encoding very large sequences where collectList()
can be problematic w.r.t. memory usage.
Comment From: sdeleuze
Answered here.