Skip to content

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

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions