Jackson has a weird behavior in handling Exceptions that occur during deserialization mapping: it throws a JsonMappingException whose .getCause() returns the innermost of the exception chain.
//in main
ObjectMapper jsonMapper = new ObjectMapper();
String json = "{\"id\": 1}";
try {
Q q = jsonMapper.readValue(json, Q.class);
} catch (JsonMappingException e) {
System.out.println(e.getCause()); //java.lang.RuntimeException: ex 2
}
//class Q
public class Q {
#JsonCreator
public Q(#JsonProperty("id") int id) {
throw new RuntimeException("ex 0",
new RuntimeException("ex 1",
new RuntimeException("ex 2")));
}
}
In the code above, I use jsonMapper.readValue(..) to map the String json to an instance of class Q whose the constructor, marked #JsonCreator, throws a chain of RuntimeException: "ex 0", "ex 1", "ex 2". When the mapping fail, I expected the line System.out.println(e.getCause()); to print out ex 0, but it prints ex 2.
Why Jackson decides to do this and is there a way to configure it so that it doesn't discard my ex 0? I have tried
jsonMapper.configure(DeserializationFeature.WRAP_EXCEPTIONS, false);
but it doesn't do anything.
Inside of Jackson's StdValueInstantiator this method gets hit when an exception is thrown during deserialization:
protected JsonMappingException wrapException(Throwable t)
{
while (t.getCause() != null) {
t = t.getCause();
}
if (t instanceof JsonMappingException) {
return (JsonMappingException) t;
}
return new JsonMappingException("Instantiation of "+getValueTypeDesc()+" value failed: "+t.getMessage(), t);
}
As you can see, this will iterate through each "level" of your nested runtime exceptions and set the last one it hits as the cause for the JsonMappingException it returns.
Here is the code I needed to get this working:
Register a new module to the ObjectMapper.
#Test
public void testJackson() {
ObjectMapper jsonMapper = new ObjectMapper();
jsonMapper.registerModule(new MyModule(jsonMapper.getDeserializationConfig()));
String json = "{\"id\": \"1\"}";
try {
Q q = jsonMapper.readValue(json, Q.class);
System.out.println(q.getId());
} catch (JsonMappingException e) {
System.out.println(e.getCause()); //java.lang.RuntimeException: ex 2
} catch (JsonParseException e) {
} catch (IOException e) {
}
}
Create a custom module class.
public class MyModule extends SimpleModule {
public MyModule(DeserializationConfig deserializationConfig) {
super("MyModule", ModuleVersion.instance.version());
addValueInstantiator(Q.class, new MyValueInstantiator(deserializationConfig, Q.class));
addDeserializer(Q.class, new CustomDeserializer());
}
}
Create a custom ValueInstantiator class to override wrapException(...). Add the instantiator to the module.
public class MyValueInstantiator extends StdValueInstantiator {
public MyValueInstantiator(DeserializationConfig config, Class<?> valueType) {
super(config, valueType);
}
#Override
protected JsonMappingException wrapException(Throwable t) {
if (t instanceof JsonMappingException) {
return (JsonMappingException) t;
}
return new JsonMappingException("Instantiation of "+getValueTypeDesc()+" value failed: "+t.getMessage(), t);
}
}
Create a custom deserializer to get the module to work properly. Add this class to the module initialization as well.
public class CustomDeserializer extends StdScalarDeserializer<Q> {
public CustomDeserializer() {
super(Q.class);
}
#Override
public Q deserialize(JsonParser jp, DeserializationContext context) throws IOException {
JsonNode node = jp.getCodec().readTree(jp);
return new Q(node.get("id").asText());
}
#Override
public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer typeDeserializer) throws IOException {
return deserialize(jp, ctxt);
}
}
For anyone looking for a different solution, this worked for me on Spring Boot 2.2.8.RELEASE. NB: This is example is when you have a rest controller with request body that is has an enum and clients could send a wrong field string gender and you want to provide proper error message:
#PostMapping
public ResponseEntity<ProfileResponse> updateProfile(#RequestBody #Valid ProfileRequest profileRequest) {
ProfileResponse profile = //do something important here that returns profile object response
return ResponseEntity
.status(HttpStatus.OK)
.body(profile);
}
ProfileRequest looks like
#Data //Lombok getters and setters
public class ProfileRequest{
private GenderEnum gender;
//some more attributes
}
Add this property to the aplication.properties file to make sure that our custom exception GlobalRuntimeException (see later for definition) is not wrapped in JsonMappingException exception.
spring.jackson.deserialization.WRAP_EXCEPTIONS=false
Then create a class which spring boot will auto create a bean for (This will be used for Deserializing the field gender of type enum). If we don't find an the enum, then we know to throw an error.
#JsonComponent
public class GenderEnumDeserializer extends JsonDeserializer<GenderEnum> {
#Override
public GenderEnum deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String val = p.getValueAsString();
GenderEnum genderEnum = GenderEnum.fromName(val);
if(genderEnum == null){
throw new GlobalRuntimeException("Invalid gender provided. Valid values are MALE | FEMALE | OTHER");
}
return genderEnum;
}
}
The "forName" method in GenderEnum looks like below.
public static GenderEnum fromName(String name) {
GenderEnum foundGenderEnum = null;
for (GenderEnum genderEnum : values()) {
if (genderEnum.name().equalsIgnoreCase(name)) {
foundGenderEnum = genderEnum;
}
}
return foundGenderEnum;
}
We would then setup catching the GlobalRuntimeException in our ControllerAdvice:
#ResponseBody
#ExceptionHandler(GlobalRuntimeException.class)
ResponseEntity<?> handleInvalidGlobalRuntimeException(HttpServletRequest request, GlobalRuntimeException ex) {
LOGGER.error("Error " + ex);
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorMessage(ex.getCustomMessage));
}
That's it.
Related
I am trying to declare a method and a constructor to RestResponse class in case of the json returns a list of DTOs directly , or probably make the existing constructor smarter to differentiate if the rest response is a List or one single element.
public class RestResponse<T> implements IRestResponse<T> {
public static ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
private T data;
private Response response;
private Exception e;
// constructor
public RestResponse(Class<T> t, Response response) {
this.response = response;
try {
this.data = t.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("There should be a default constructor in the Response POJO");
}
// getBody method
public T getBody() {
try {
data = (T) response.getBody().as(data.getClass());
} catch (Exception e) {
this.e = e;
}
return data;
}
}
}
This implementation is very Ok when the return response is a single DTO like this:
{
"attribute1":"value1",
"attribute2":"value2",
"attribute3":"value3"
}
or even like this:
{
"attribute1":"value1",
"attribute2":"value2",
"attribute3":[
{ "att1":"val1",
"att2":"val2"
}]
}
but I can't get a generic solution to get a method when the return is like:
[
{
"attribute1":"value1",
"attribute2":"value2",
"attribute3":"value3"
},{
"attribute1":"value1",
"attribute2":"value2",
"attribute3":"value3"
}
]
I'm having trouble converting a List object and it's returning the error below:
expected S in value {L: [{M: {txt_just={S: new client,}, num_funl={S: 123,}, num_propt={S: 2f1a8e6c-68bb-4c26-9326-3823d9f96c4c,}, ind_decs={S: S,}, dat_hor={S: 20220721183000,}},}],}
My Class translator:
public class ObjectTranslators<T> implements DynamoDBTypeConverter<String, T> {
private static final ObjectMapper mapper = new ObjectMapper();
#Override
public String convert(T t) {
try {
return mapper.writeValueAsString(t);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Unable to parse JSON");
}
}
#Override
public T unconvert(String s) {
try {
return mapper.readValue(s, new TypeReference<>() {});
} catch (IOException e) {
throw new IllegalArgumentException("Unable to read JSON");
}
}
}
You are trying to convert a List that has a single entry which is a Map of 5 key/value pairs. Your real issue is probably that there is no value supplied to ind_decs but, it's also possible that you aren't properly using the expected generic T type wherever ObjectTranslators is used as well.
expected S in value
{
L: [
{
M: {
txt_just={S: new client,},
num_funl={S: 123,},
num_propt={S: 2f1a8e6c-68bb-4c26-9326-3823d9f96c4c,},
ind_decs={S: S,},
dat_hor={S: 20220721183000,}
},
}
],
}
I've made a generic method that is convertor class for complex classes and 2nd one for enums. I have Recipe class that is complex so I used #DynamoDBTypeConverted(converter = ObjectConverter.class)
This is my converter class:
public class ObjectConverter<T extends Object> implements DynamoDBTypeConverter<String, T> {
ObjectMapper objectMapper = new ObjectMapper();
#Override
public String convert(T object) {
try {
return objectMapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
throw new IllegalArgumentException("Unable to parse JSON");
}
#Override
public T unconvert(String object) {
try {
T unconvertedObject = objectMapper.readValue(object, new TypeReference<T>() {
});
return unconvertedObject;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
This is convertor class for enums:
public class EnumConverter<T extends Object> implements DynamoDBTypeConverter<String, List<T>> {
#Override
public String convert(List<T> objects) {
//Jackson object mapper
ObjectMapper objectMapper = new ObjectMapper();
try {
String objectsString = objectMapper.writeValueAsString(objects);
return objectsString;
} catch (JsonProcessingException e) {
//do something
}
return null;
}
#Override
public List<T> unconvert(String objectsString) {
ObjectMapper objectMapper = new ObjectMapper();
try {
List<T> objects = objectMapper.readValue(objectsString, new TypeReference<List<T>>() {
});
return objects;
} catch (JsonParseException e) {
//do something
} catch (JsonMappingException e) {
//do something
} catch (IOException e) {
//do something
}
return null;
}
The problem is when I try to test CRUDs methods.. I have addProduct method and this one works fine, I created addRecipe method and it looks almost the same, but here I have problem while posting in Postman i got an error: "Bad request, unable to parse JSON".
And information from log file:
"Can not deserialize instance of java.util.ArrayList out of START_OBJECT token at [Source: {"id":null,"name":"test","labels":["GLUTEN_FREE"],"author":{"name":"Plejer Annołn","id":"testID2"},"media":{"name":"heheszki","url":"http://blabla.pl","mediaType":"IMAGE"},"recipeElements":{"product":{"id":927c3ed3-400b-433d-9da0-1aa111dce584,"name":"bąkiKacpraNieŚmierdzą","calories":1000,"fat":400.0,"carbo":20.0,"protein":40.0,"productKinds":["MEAT"],"author":{"name":"Plejer Annołn","id":"testID2"},"media":{"name":"heheszki","url":"http://blabla.pl","mediaType":"IMAGE"},"approved":false},"weight":"100"},"approved":false}; line: 1, column: 190] (through reference chain: pl.javamill.model.kitchen.Recipe["recipeElements"])"
What can be wrong?
The methods in the converter class are always returning a value even if exceptions are thrown (unless they are RuntimeExceptions), though they may not be correctly marshaling/unmarshaling the Product in RecipeElement. A better alternative is to annotate the getRecipeElement() method in your class with #DynamoDBTypeConvertedJson, that provides out-of-the-box JSON marshaling/unmarshaling. It may be something to do with the HTTP request you are sending in Postman too. You should add more information on the getProduct(), setProduct() methods and the actual postman request (without any sensitive information).
I am trying to set a field from the outer class within an asynchronous class but it is not working for me.
public class FlinkJsonObject {
TrafficData jsonObject;
ObjectMapper mapper = new ObjectMapper();
public FlinkJsonObject(String url, int port) throws URISyntaxException {
final WebsocketClientEndpoint clientEndPoint = new WebsocketClientEndpoint(new URI("wss://city.up.us/outbound/SPPAnalyticsStatement"));
clientEndPoint.addMessageHandler(new WebsocketClientEndpoint.MessageHandler() {
#Override
public void handleMessage(String message) {
try {
// Using this does not work here
this.jsonObject = mapper.readValue(message, TrafficData.class);
} catch (IOException ex) {
Logger.getLogger(FlinkJsonObject.class.getName()).log(Level.SEVERE, null, ex);
}
}
});
}
}
I have tried setting the field using an external method and calling it in the asynchronous class but it does not work for me.
I have the follow the following filter:
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
ServletException {
try {
chain.doFilter(new XSSRequestWrapper((HttpServletRequest) request), response);
} catch (XssAttackException e) {
request.getRequestDispatcher("/XssAttack").forward(request, response);
}
}
and the class XssAttackException is:
public class XssAttackException extends RuntimeException {
private static final long serialVersionUID = 1L;
}
after debugging the code, I realized that somewhere in the spring framework all the exceptions are being caught. Now I need a way that my catch bock also run.
UPDATE
inside XSSRequestWrapper we have:
#Override
public String getHeader(String name) {
String value = super.getHeader(name);
return stripXSS(value);
}
And
private String stripXSS(String value) {
if (value != null) {
value = persianUtf8(value);
if (!value.equals(Jsoup.parse(value).text())) {
throw new XssAttackException();
}
value = Jsoup.parse(value).text();
for (Pattern scriptPattern : patterns) {
if (scriptPattern.matcher(value).matches()) {
throw new XssAttackException();
}
value = scriptPattern.matcher(value).replaceAll("");
}
}
return value;
}
Please don't assume this is answer for your question.Assumed too long comment.
I created my CustomException class.
public class CustomException extends RuntimeException {
}
and created custom Servlet class as your XSSRequestWrapper and throw my custom exception in constructor.
public class MyServlet implements ServletRequest {
public MyServlet() {
throw new CustomException();
}
// other override methods go here
}
and in my filter class
try {
chain.doFilter(new MyServlet(), response);
} catch (CustomException e) {
System.out.println("xxxxxxxxxxxxxxx I got it xxxxxxxxxxxxxxxxxxx");
}
This code work fine. At your program , I think there has some exception has occured and you did not catch on them. So , this exception object has miss from your try block of your filter class and handled by Spring container.