jackson-databind icon indicating copy to clipboard operation
jackson-databind copied to clipboard

Cannot deserialize Throwables with Jackson databind 2.9.4 using @JsonIdentityInfo

Open sholavanalli opened this issue 6 years ago • 8 comments

I have written a test case that reproduces this issue. Object mapper configuration:

`

private ObjectMapperAccess() {
    this.objectMapper = new ObjectMapper();

    /**
     * Generic configuration
     */
    this.objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
    this.objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
    this.objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

    /**
     * Configure deserialization features
     */
    this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

    /**
     * Register mixins
     */
    this.objectMapper.addMixIn(Throwable.class, ThrowablelMixin.class);
    this.objectMapper.addMixIn(StackTraceElement.class, StackTraceElementMixin.class);
}`

Mixin definitions:

@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class) public abstract class ThrowablelMixin {}

public abstract class StackTraceElementMixin { // With this property name and getter name will be the same. @JsonProperty("className") private String declaringClass; }

Test case that fails:

`

private void generateException() {
    try {
        Integer.parseInt("invalid_int");
    } catch (NumberFormatException e) {
        e.printStackTrace();
        throw new RmiHttpRuntimeException("Could not parse", e);
    }
}

/**
 * Test mixin for StackTraceElement.
 */
@Test
public void testStackTraceElementAndThrowableMixin() throws IOException {
    try {
        generateException();
        Assert.fail(String.format("%s did not occur.", NumberFormatException.class.getName()));
    } catch (RmiHttpRuntimeException re) {
        re.printStackTrace();
        String json = objectMapper.writeValueAsString(re);
        System.out.println(json);
        RmiHttpRuntimeException pre = objectMapper.readValue(json, RmiHttpRuntimeException.class);
        Assert.assertFalse(pre.getStackTrace()[0].getClassName().isEmpty());
        //TODO figure out a way to de/serialize cause.
        Assert.assertNotNull(pre.getCause().getStackTrace()[0].getClassName());
    }
}`

Exception message and stacktrace of the above test case:

com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Missing type id when trying to resolve subtype of [simple type, class java.lang.Throwable]: missing type id property '@class' (for POJO property 'cause') at [Source: (String)"{"@class":"com.netapp.oci.platform.http.RmiHttpRuntimeException","@id":1,"detailMessage":"Could not parse","cause":{"@class":"java.lang.NumberFormatException","@id":2,"detailMessage":"For input string: "invalid_int"","cause":2,"stackTrace":[{"methodName":"forInputString","fileName":"NumberFormatException.java","lineNumber":65,"className":"java.lang.NumberFormatException"},{"methodName":"parseInt","fileName":"Integer.java","lineNumber":580,"className":"java.lang.Integer"},{"methodName":"parseIn"[truncated 6895 chars]; line: 1, column: 228]

at com.fasterxml.jackson.databind.exc.InvalidTypeIdException.from(InvalidTypeIdException.java:43)
at com.fasterxml.jackson.databind.DeserializationContext.missingTypeIdException(DeserializationContext.java:1638)
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingTypeId(DeserializationContext.java:1217)
at com.fasterxml.jackson.databind.jsontype.impl.TypeDeserializerBase._handleMissingTypeId(TypeDeserializerBase.java:300)
at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedUsingDefaultImpl(AsPropertyTypeDeserializer.java:164)
at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:88)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:1171)
at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:527)
at com.fasterxml.jackson.databind.deser.std.ThrowableDeserializer.deserializeFromObject(ThrowableDeserializer.java:103)
at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:194)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:161)
at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:130)
at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:97)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:1171)
at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:527)
at com.fasterxml.jackson.databind.deser.std.ThrowableDeserializer.deserializeFromObject(ThrowableDeserializer.java:103)
at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:194)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:161)
at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:130)
at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:97)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:1171)
at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:68)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4001)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2992)
at com.netapp.oci.platform.http.jackson.TestObjectMapper.testStackTraceElementMixin(TestObjectMapper.java:116)

sholavanalli avatar Mar 30 '18 14:03 sholavanalli

Ok, to make this simple, I will state that @JsonIdentityInfo does not currently work with Throwables. It is quite likely it never will, either: identity info works only with POJOs, and is typically ignored for scalar values.

I will leave the issue open but don't want anyone to get their hopes up: do not try using this combination of features.

cowtowncoder avatar Mar 30 '18 20:03 cowtowncoder

@cowtowncoder Thanks for the reply. Can you advise a work around for this?

sholavanalli avatar Mar 30 '18 22:03 sholavanalli

@sholavanalli I think the main question is that of why @JsonIdentityInfo is being used. Would it be possible to just eliminate it?

cowtowncoder avatar Mar 30 '18 23:03 cowtowncoder

@cowtowncoder Sorry for not being clear. If i don't use @JsonIdentityInfo then object mapper fails to serialize Throwable with this exception:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-reference leading to cycle (through reference chain: com.netapp.oci.platform.http.RmiHttpRuntimeException["cause"]->java.lang.NumberFormatException["cause"])

at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77)
at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1191)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter._handleSelfReference(BeanPropertyWriter.java:944)

sholavanalli avatar Apr 04 '18 03:04 sholavanalli

@sholavanalli in that case you should probably try to prevent serialization of cause field. But more importantly, isn't that a rather badly initialized exception object? cause should never be object itself should it?

cowtowncoder avatar Apr 04 '18 04:04 cowtowncoder

@cowtowncoder Let me give you some background on what i am doing. I am converting a legacy EJB into a JAX-RS based REST interface. The EJB declares that it throws an exception and my job is to make sure the exception gets deserialized back into the original object on the client side including the cause. If i ignore the cause then i will be breaking compatibility. The errors i mentioned above only appears with this particular configuration of the object mapper. Otherwise causes get de/serialized without any issues.

sholavanalli avatar Apr 04 '18 12:04 sholavanalli

@sholavanalli Ok: what I am saying now is that your approach of @JsonIdentityInfo is unlikely to work any time soon. But I have given enough information for you to figure out some other way to try to prevent the problem -- the root problem is exactly is exception suggest: cause of a Throwable points to itself. That is wrong and really should be fixed.

You could, for example, add @JsonIgnoreProperties({ "cause" }) in your exception mix-in to avoid cause from being added, ever, on serialization.

I will leave the issue open as it would be nice to support identity info on Throwables too, but it is not high on list to work on.

cowtowncoder avatar Apr 04 '18 18:04 cowtowncoder

cause of a Throwable points to itself. That is wrong and really should be fixed.

It is not wrong. It is the default behavior of Throwable when no cause is given.

From Throwable.java:

/**
 * The throwable that caused this throwable to get thrown, or null if this
 * throwable was not caused by another throwable, or if the causative
 * throwable is unknown.  If this field is equal to this throwable itself,
 * it indicates that the cause of this throwable has not yet been
 * initialized.
 *
 * @serial
 * @since 1.4
 */
private Throwable cause = this;

...

public synchronized Throwable getCause() {
    return (cause==this ? null : cause);
}

kreiger avatar Apr 10 '19 12:04 kreiger