jackson-databind
                                
                                
                                
                                    jackson-databind copied to clipboard
                            
                            
                            
                        java.lang.IllegalArgumentException is serializable then deserializable but my own similar exception is not
Search before asking
- [x] 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.