Skip to content

Custom Throwable not serializable if using JsonAutoDetect settings that only detect Fields #5194

@riskop

Description

@riskop

Search before asking

  • I searched in the issues and found nothing similar.

Describe the bug

I am trying to serialize and then deserialize an exception. It works in case of a java.lang.IllegalArgumentException, but it does not work for my -- identical -- riskop.MyIllegalArgumentException.

Version Information

Spring boot 3.5.0, Jackson 2.19.0

Reproduction

This is my ObjectMapper config:

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
        objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
        PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
                        .allowIfSubType(Object.class)
                        .build();
        objectMapper.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

This is how I serialize / deserialize:

    public void toFromJson(Throwable in) throws Throwable {
        String json1 = objectMapper.writeValueAsString(in);
        Throwable out = objectMapper.readValue(json1, Throwable.class);
        String json2 = objectMapper.writeValueAsString(out);
        throw out;
    }

This is working:

        IllegalArgumentException e1 = new IllegalArgumentException();
        try {
            toFromJson.toFromJson(e1);
        }
        catch (IllegalArgumentException e2) {
            // okay, expected
            Assertions.assertEquals(e1.getMessage(), e2.getMessage());
        }
        catch (Throwable t) {
            throw new RuntimeException(t);
        }

This is not working, but I think it should:

        MyIllegalArgumentException e1 = new MyIllegalArgumentException();
        try {
            toFromJson.toFromJson(e1);
        }
        catch (MyIllegalArgumentException e2) {
            // okay, expected
            Assertions.assertEquals(e1.getMessage(), e2.getMessage());
        }
        catch (Throwable t) {
            throw new RuntimeException(t);
        }

There is a mini project for demonstrating the problem:
https://github.com/riskop/jackson_throwable_to_from_json

There's no difference between MyIllegalArgumentException and the JDK's one:

JDK's IllegalArgumentException:
https://github.com/openjdk/jdk21/blob/master/src/java.base/share/classes/java/lang/IllegalArgumentException.java

My MyIllegalArgumentException
https://github.com/riskop/jackson_throwable_to_from_json/blob/main/src/main/java/riskop/MyIllegalArgumentException.java

just execute
mvn clean install

stacktrace:

1619 [INFO]  T E S T S
1619 [INFO] -------------------------------------------------------
1939 [INFO] Running riskop.MyTest
11:08:44.999 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [riskop.MyTest]: MyTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
11:08:45.056 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration riskop.Start for test class riskop.MyTest

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.5.0)

2025-06-17T11:08:45.273+02:00  INFO 524080 --- [           main] riskop.MyTest                            : Starting MyTest using Java 21.0.1 with PID 524080 (started by riskop in /home/riskop/IdeaProjects/jackson_throwable_to_from_json)
2025-06-17T11:08:45.274+02:00  INFO 524080 --- [           main] riskop.MyTest                            : No active profile set, falling back to 1 default profile: "default"
2025-06-17T11:08:45.625+02:00  INFO 524080 --- [           main] riskop.MyTest                            : Started MyTest in 0.503 seconds (process running for 0.971)
Mockito is currently self-attaching to enable the inline-mock-maker. This will no longer work in future releases of the JDK. Please add Mockito as an agent to your build as described in Mockito's documentation: https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html#0.3
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
WARNING: A Java agent has been loaded dynamically (/home/riskop/.m2/repository/net/bytebuddy/byte-buddy-agent/1.17.5/byte-buddy-agent-1.17.5.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release
3103 [ERROR] Tests run: 2, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 1.152 s <<< FAILURE! -- in riskop.MyTest
3103 [ERROR] riskop.MyTest.TestMyIAE -- Time elapsed: 0.007 s <<< ERROR!
java.lang.RuntimeException: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-reference leading to cycle (through reference chain: riskop.MyIllegalArgumentException["cause"])
        at riskop.MyTest.TestMyIAE(MyTest.java:40)
        at java.base/java.lang.reflect.Method.invoke(Method.java:580)
        at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
        at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-reference leading to cycle (through reference chain: riskop.MyIllegalArgumentException["cause"])
        at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77)
        at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1359)
        at com.fasterxml.jackson.databind.ser.BeanPropertyWriter._handleSelfReference(BeanPropertyWriter.java:953)
        at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:726)
        at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:760)
        at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeWithType(BeanSerializerBase.java:643)
        at com.fasterxml.jackson.databind.ser.impl.TypeWrappedSerializer.serialize(TypeWrappedSerializer.java:32)
        at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:503)
        at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:342)
        at com.fasterxml.jackson.databind.ObjectMapper._writeValueAndClose(ObjectMapper.java:4859)
        at com.fasterxml.jackson.databind.ObjectMapper.writeValueAsString(ObjectMapper.java:4079)
        at riskop.ToFromJson.toFromJson(ToFromJson.java:18)
        at riskop.MyTest.TestMyIAE(MyTest.java:33)
        ... 3 more

3118 [INFO] 
3118 [INFO] Results:
3118 [INFO] 

Expected behavior

No response

Additional context

background:

I would like to rewrite a http-invoker based communication framework with REST technology. Http-invoker handled exceptions transparently, if the server side caused an exception, the client side just received it.

I am trying to reproduce that behaviour. In case of errors the server component would serialize the exception to json, pass it to the client ( with rest ) then the client would restore the original exception from json.

Metadata

Metadata

Assignees

No one assigned

    Labels

    2.20Issues planned at 2.20 or laterhas-failing-testIndicates that there exists a test case (under `failing/`) to reproduce the issue

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions