Serializing/deserializing exceptions without stack trace using Jackson

我们两清 提交于 2021-01-27 13:32:04

问题


I am trying to create a class with a java.lang.Exception stored as a field. Also I am trying to exclude stack trace from serialization/deserialization using @JsonIgnoreProperties annotation.

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;

class ExWrapper {
    @JsonIgnoreProperties({"stackTrace"})
    public Exception ex;

    @Override
    public String toString() {
        return "ExWrapper{" +
                "ex=" + ex +
                '}';
    }
}

public class Example {
    public static void main(String[] args) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        ExWrapper exw = new ExWrapper();
        exw.ex = new Exception("Oops");
        String str = mapper.writeValueAsString(exw);
        System.out.println(str);
        ExWrapper exW = mapper.readValue(str, ExWrapper.class);
        System.out.println(exW);
    }
}

The result error is quite surprising, Jackson cannot find the message field:

Exception in thread "main" com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "message" (class java.lang.Exception), not marked as ignorable (one known property: "cause"])
 at [Source: (String)"{"ex":{"cause":null,"message":"Oops","suppressed":[],"localizedMessage":"Oops"}}"; line: 1, column: 32] (through reference chain: ExWrapper["ex"]->java.lang.Exception["message"])
    at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:61)
    at com.fasterxml.jackson.databind.DeserializationContext.handleUnknownProperty(DeserializationContext.java:840)
    at com.fasterxml.jackson.databind.deser.std.StdDeserializer.handleUnknownProperty(StdDeserializer.java:1179)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperty(BeanDeserializerBase.java:1592)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownVanilla(BeanDeserializerBase.java:1570)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:375)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159)
    at com.fasterxml.jackson.databind.deser.impl.FieldProperty.deserializeAndSet(FieldProperty.java:138)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:288)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4202)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3205)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3173)
    at Example.main(Example.java:25)

Well, there is only getMessage and no setMessage in Throwable class so it seems a bit reasonable unless you try to remove @JsonIgnoreProperties annotation. It works like a charm: it serializes and deserializes back properly and missing setter for message is suddenly not a problem. Adding "message" to ignored fields also make it work (yet without an exception message).

I tried to randomly step in Jackson code using debugger and found out that when @JsonIgnoreProperties is missing, eventually ThrowableDeserializer methods are called and they are not called when the annotation is present. ThrowableDeserializer seems to have some hacks specific to exception message. My guess is that ThrowableDeserializer is not viable when the stack trace is missing and Jackson falls back to default java bean serializer.

The question is what exactly is going on here and how to solve it.


回答1:


Version 2

ThrowableDeserializer class extends BeanDeserializer so these two share some code how to create and deserialise a POJO. Exception is not a regular POJO and should be handled differently. Since it does not provide to many setters we need to use constructor to create it with message and other fields we can skip. To register constructor we can use MixIn feature:

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.File;

public class JsonPathApp {

    public static void main(String[] args) throws Exception {
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();

        ObjectMapper mapper = new ObjectMapper();
        mapper.addMixIn(Exception.class, ExceptionMixIn.class);
        mapper.addMixIn(Throwable.class, ThrowableMixIn.class);

        ExWrapper exW = mapper.readValue(jsonFile, ExWrapper.class);
        exW.ex.printStackTrace();
    }
}

@JsonIgnoreProperties("stackTrace")
abstract class ExceptionMixIn extends Exception {

    @JsonCreator
    public ExceptionMixIn(@JsonProperty("message") String message) {
        super(message);
    }
}

@JsonIgnoreProperties("stackTrace")
abstract class ThrowableMixIn extends Throwable {

    @JsonCreator
    public ThrowableMixIn(@JsonProperty("message") String message) {
        super(message);
    }
}

class ExWrapper {

    public Exception ex;

    @Override
    public String toString() {
        return "ExWrapper{" +
                "ex=" + ex +
                '}';
    }
}

Above code for a JSON with exception with a cause prints:

java.lang.Exception: Opps
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
    at com.fasterxml.jackson.databind.introspect.AnnotatedConstructor.call(AnnotatedConstructor.java:124)
    at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createFromObjectWith(StdValueInstantiator.java:283)
    at com.fasterxml.jackson.databind.deser.ValueInstantiator.createFromObjectWith(ValueInstantiator.java:229)
    at com.fasterxml.jackson.databind.deser.impl.PropertyBasedCreator.build(PropertyBasedCreator.java:195)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:422)
    at com.fasterxml.jackson.databind.deser.std.ThrowableDeserializer.deserializeFromObject(ThrowableDeserializer.java:65)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159)
    at com.fasterxml.jackson.databind.deser.impl.FieldProperty.deserializeAndSet(FieldProperty.java:138)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:288)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4013)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2902)
Caused by: java.lang.Throwable: Root oops
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
    at com.fasterxml.jackson.databind.introspect.AnnotatedConstructor.call(AnnotatedConstructor.java:124)
    at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createFromObjectWith(StdValueInstantiator.java:283)
    at com.fasterxml.jackson.databind.deser.ValueInstantiator.createFromObjectWith(ValueInstantiator.java:229)
    at com.fasterxml.jackson.databind.deser.impl.PropertyBasedCreator.build(PropertyBasedCreator.java:195)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:422)
    at com.fasterxml.jackson.databind.deser.std.ThrowableDeserializer.deserializeFromObject(ThrowableDeserializer.java:65)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159)
    at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:530)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:528)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:449)
    ... 8 more

As you can see, exception has default stackTrace and stackTrace from JSON payload is skipped.

Version 1

I did not dig it too much but enabling allowSetters solves this problem:

@JsonIgnoreProperties(value = {"stackTrace"}, allowSetters = true)
public Exception ex;

Your code with this change prints:

{"ex":{"cause":null,"message":"Oops","localizedMessage":"Oops","suppressed":[]}}
ExWrapper{ex=java.lang.Exception: Oops}


来源:https://stackoverflow.com/questions/58530206/serializing-deserializing-exceptions-without-stack-trace-using-jackson

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!