Skip to content

Allows Jackson2 encoders to log Throwable reason for not being able to serialize or deserialize #25892

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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
Expand Down Expand Up @@ -94,13 +95,38 @@ public int getMaxInMemorySize() {
return this.maxInMemorySize;
}

/**
* Called by {@link #canDecode(ResolvableType, MimeType)} when
* {@link ObjectMapper#canDeserialize(JavaType, AtomicReference)} produces
* an exception. {@link ObjectMapper} produces exceptions that are helpful
* to debug why Jackson can't decode a certain message, but swallows them
* and just returns false. Default implementation does nothing, but
* subclasses can choose to override.
* @param elementType the actual target type to decode to that was passed
* to canDecode()
* @param mimeType the element type to decode to that was passed to
* canDecode()
* @param throwable the throwable produced by Jackson explaining why
* it couldn't be decoded
*/
protected void reportCanDecodeThrowable(ResolvableType elementType, @Nullable MimeType mimeType,
Throwable throwable) {
}

@Override
public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) {
AtomicReference<Throwable> throwableReference = new AtomicReference<>();
JavaType javaType = getObjectMapper().constructType(elementType.getType());
// Skip String: CharSequenceDecoder + "*/*" comes after
return (!CharSequence.class.isAssignableFrom(elementType.toClass()) &&
getObjectMapper().canDeserialize(javaType) && supportsMimeType(mimeType));
boolean canDecode = (!CharSequence.class.isAssignableFrom(elementType.toClass()) &&
getObjectMapper().canDeserialize(javaType, throwableReference) && supportsMimeType(mimeType));

Throwable throwable = throwableReference.get();
if (throwable != null) {
reportCanDecodeThrowable(elementType, mimeType, throwable);
}

return canDecode;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonGenerator;
Expand Down Expand Up @@ -98,6 +99,23 @@ public void setStreamingMediaTypes(List<MediaType> mediaTypes) {
this.streamingMediaTypes.addAll(mediaTypes);
}

/**
* Called by {@link #canEncode(ResolvableType, MimeType)} when
* {@link ObjectMapper#canSerialize(Class, AtomicReference)} produces
* an exception. {@link ObjectMapper} produces exceptions that are helpful
* to debug why Jackson can't encode a certain message, but swallows them
* and just returns false. Default implementation does nothing, but
* subclasses can choose to override.
* @param elementType the actual source type to encode that was passed to
* canEncode()
* @param mimeType the element type that we're trying to encode that was
* passed to canEncode()
* @param throwable the throwable produced by Jackson explaining why
* it couldn't be encoded
*/
protected void reportCanEncodeThrowable(ResolvableType elementType, @Nullable MimeType mimeType,
Throwable throwable) {
}

@Override
public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) {
Expand All @@ -111,8 +129,17 @@ public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType
return false;
}
}
return (Object.class == clazz ||
(!String.class.isAssignableFrom(elementType.resolve(clazz)) && getObjectMapper().canSerialize(clazz)));
AtomicReference<Throwable> throwableReference = new AtomicReference<>();
boolean canEncode = (Object.class == clazz ||
(!String.class.isAssignableFrom(elementType.resolve(clazz)) &&
getObjectMapper().canSerialize(clazz, throwableReference)));

Throwable throwable = throwableReference.get();
if (throwable != null) {
reportCanEncodeThrowable(elementType, mimeType, throwable);
}

return canEncode;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,18 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.junit.jupiter.api.Test;
import org.mockito.BDDMockito;
import org.mockito.Mockito;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
Expand All @@ -45,6 +49,7 @@
import org.springframework.http.MediaType;
import org.springframework.http.codec.json.JacksonViewBean.MyJacksonView1;
import org.springframework.http.codec.json.JacksonViewBean.MyJacksonView3;
import org.springframework.lang.Nullable;
import org.springframework.util.MimeType;
import org.springframework.web.testfixture.xml.Pojo;

Expand Down Expand Up @@ -274,6 +279,35 @@ public void decodeAscii() {
null);
}

@Test
public void canDecodeReportsException() {
ResolvableType pojoType = ResolvableType.forClass(Pojo.class);
MimeType jsonType = APPLICATION_JSON;

Throwable expected = new IllegalArgumentException("Fake exception thrown by ObjectMapper.");
AtomicReference<Throwable> actual = new AtomicReference<>();

ObjectMapper objectMapper = Mockito.mock(ObjectMapper.class);
BDDMockito.given(objectMapper.canDeserialize(BDDMockito.nullable(JavaType.class), BDDMockito.any())).will(invocation -> {
AtomicReference<Throwable> throwableReference = invocation.getArgument(1);
throwableReference.set(expected);
return false;
});

Jackson2JsonDecoder reportingEncoder = new Jackson2JsonDecoder(objectMapper) {
@Override
protected void reportCanDecodeThrowable(ResolvableType elementType, @Nullable MimeType mimeType,
Throwable throwable) {
if (pojoType.equals(elementType) && jsonType.equals(mimeType)) {
actual.set(throwable);
}
}
};

assertThat(reportingEncoder.canDecode(pojoType, jsonType)).isFalse();
assertThat(actual.get()).isEqualTo(expected);
}


private Mono<DataBuffer> stringBuffer(String value) {
return stringBuffer(value, StandardCharsets.UTF_8);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,17 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.junit.jupiter.api.Test;
import org.mockito.BDDMockito;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
Expand All @@ -39,6 +44,7 @@
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.http.codec.json.JacksonViewBean.MyJacksonView1;
import org.springframework.http.codec.json.JacksonViewBean.MyJacksonView3;
import org.springframework.lang.Nullable;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.testfixture.xml.Pojo;
Expand Down Expand Up @@ -240,6 +246,35 @@ public void encodeAscii() {

}

@Test
public void canEncodeReportsException() {
ResolvableType pojoType = ResolvableType.forClass(Pojo.class);
MimeType jsonType = APPLICATION_JSON;

Throwable expected = new IllegalArgumentException("Fake exception thrown by ObjectMapper.");
AtomicReference<Throwable> actual = new AtomicReference<>();

ObjectMapper objectMapper = Mockito.mock(ObjectMapper.class);
BDDMockito.given(objectMapper.canSerialize(BDDMockito.eq(Pojo.class), BDDMockito.any())).will(invocation -> {
AtomicReference<Throwable> throwableReference = invocation.getArgument(1);
throwableReference.set(expected);
return false;
});

Jackson2JsonEncoder reportingEncoder = new Jackson2JsonEncoder(objectMapper) {
@Override
protected void reportCanEncodeThrowable(ResolvableType elementType, @Nullable MimeType mimeType,
Throwable throwable) {
if (pojoType.equals(elementType) && jsonType.equals(mimeType)) {
actual.set(throwable);
}
}
};

assertThat(reportingEncoder.canEncode(pojoType, jsonType)).isFalse();
assertThat(actual.get()).isEqualTo(expected);
}


@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
private static class ParentClass {
Expand Down