I know it's been a while since the answer was accepted but I recently stumbled onto an issue with Jackson that allowed me to discover a problem with using ResponseBodyAdvice
.
Jackson will not correctly serialize your polymorphic types that use @JsonTypeInfo
/ @JsonSubTypes
if during runtime the values of your types are not known: i.e. for example if you have a generic container type like class ResponseWrapper<T> { List<T> objects; }
. That is unless you provide Jackson with specialization of that generic type before you ask it to serialize your value, refer to Why does Jackson polymorphic serialization not work in lists? . Spring does this for you when you return say a list of T and that T
is known because it's provided explicitly in the method return type (as in public List<MyEntity> getAllEntities();
).
If you simply implement ResponseBodyAdvice
and return a new, wrapped value from beforeBodyWrite()
then Spring will no longer know your full generic type with its specialization, and it will serialize your response as ResponseWrapper<?>
instead of ResponseWrapper<MyEntity>
.
The only way around this is to both extend from AbstractJackson2HttpMessageConverter
and override writeInternal()
. See how the method treats the type here: https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java#L437
And you also need to implement a Controller advice using AbstractMappingJacksonResponseBodyAdvice
and your own custom MappingJacksonValue
that includes Type targetType
that custom HttpMessageConverter will use.
ResponseWrapper
public class ResponseWrapper<T> {
@Nullable Error error;
T result;
public ResponseWrapper(T result) {
this.result = result;
}
}
WrappingAdvice
@Component
public class WrappingAdvice extends AbstractMappingJacksonResponseBodyAdvice {
@Override
protected MappingJacksonValue getOrCreateContainer(Object body) {
MappingJacksonValue cnt = super.getOrCreateContainer(body);
if (cnt instanceof MyMappingJacksonValue) {
return cnt;
}
return new MyMappingJacksonValue(cnt);
}
@Override
protected void beforeBodyWriteInternal(
MappingJacksonValue bodyContainer, MediaType contentType,
MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {
MyMappingJacksonValue cnt = (MyMappingJacksonValue) bodyContainer;
Type targetType = getTargetType(bodyContainer.getValue(), returnType);
cnt.setValue(new ResponseWrapper(cnt.getValue()));
cnt.setTargetType(TypeUtils.parameterize(
ResponseWrapper.class,
targetType));
}
/**
* This is derived from AbstractMessageConverterMethodProcessor
*/
private Type getTargetType(Object value, MethodParameter returnType) {
if (value instanceof CharSequence) {
return String.class;
}
Type genericType;
if (HttpEntity.class.isAssignableFrom(returnType.getParameterType())) {
genericType = ResolvableType.forType(returnType.getGenericParameterType()).getGeneric().getType();
} else {
genericType = returnType.getGenericParameterType();
}
return GenericTypeResolver.resolveType(genericType, returnType.getContainingClass());
}
public static class MyMappingJacksonValue extends MappingJacksonValue {
private Type targetType;
public MyMappingJacksonValue(MappingJacksonValue other) {
super(other.getValue());
setFilters(other.getFilters());
setSerializationView(other.getSerializationView());
}
public Type getTargetType() {
return targetType;
}
public void setTargetType(Type targetType) {
this.targetType = targetType;
}
}
}
JsonHttpMessageBodyConverter
@Component
public class JsonHttpMessageBodyConverter extends AbstractJackson2HttpMessageConverter {
// omitted all constructors
@Override
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
if (object instanceof WrapAPIResponseAdvice.MyMappingJacksonValue) {
type = ((WrapAPIResponseAdvice.MyMappingJacksonValue) object).getTargetType();
}
super.writeInternal(object, type, outputMessage);
}
}