Skip to content

[BUG] Jackson 3 FailedState.doNotRetry deserialization fails on GraalVM Native Image #1493

@blogcin

Description

@blogcin

JobRunr Version

8.4.2 (master branch, reproducible since Jackson 3 support was added)

JDK Version

GraalVM CE 21+ (Native Image). Works correctly on HotSpot JVM.

Your SQL / NoSQL database

Any (not database-specific — the bug is in JSON deserialization)

What happened?

When deserializing FailedState on GraalVM Native Image, the primitive boolean doNotRetry field causes:

IllegalArgumentException: Can not set boolean field org.jobrunr.jobs.states.FailedState.doNotRetry to java.lang.Integer

Expected: FailedState deserializes correctly with doNotRetry preserving its true/false value.

Root cause: Jackson 3 (tools.jackson.databind) uses MethodHandle.invokeExact for field setting. On GraalVM Native Image, the MethodHandle adaptation chain that converts (Object, Object)V(FailedState, boolean)V has a bug that corrupts Boolean into Integer before reaching UnsafeBooleanFieldAccessorImpl.set().

Key observations:

  • The deserialized value itself is correctly java.lang.Boolean — Jackson's error handler confirms this.
  • The corruption happens inside the MethodHandle chain, not during deserialization.
  • Both true/false values fail equally — this is not a coercion issue.
  • CoercionConfig(Integer → Boolean) does NOT fix it (confirmed — see branch).

How to reproduce?

  1. Create a FailedState with doNotRetry = true (e.g., from a JobRunrException with isProblematicAndDoNotRetry())
  2. Serialize to JSON with Jackson3JsonMapper
  3. Deserialize back on GraalVM Native Image
Jackson3JsonMapper jsonMapper = new Jackson3JsonMapper();

// Round-trip: serialize then deserialize
JobRunrException exception = JobRunrException.problematicConfigurationException("Problematic config");
FailedState original = new FailedState("Job failed", exception);

String json = jsonMapper.serialize(original);
FailedState deserialized = jsonMapper.deserialize(json, FailedState.class);
// ^ throws IllegalArgumentException on GraalVM Native Image

// Direct JSON deserialization also fails:
String directJson = """
    {
      "@class": "org.jobrunr.jobs.states.FailedState",
      "state": "FAILED",
      "createdAt": "2024-01-15T10:30:00Z",
      "message": "Job failed",
      "exceptionType": "org.jobrunr.JobRunrException",
      "exceptionMessage": "Problematic config",
      "stackTrace": "org.jobrunr.JobRunrException: Problematic config",
      "doNotRetry": true
    }
    """;
FailedState fromJson = jsonMapper.deserialize(directJson, FailedState.class);
// ^ same IllegalArgumentException on Native Image

A test suite to reproduce this bug is available in my fork:
tests/e2e-native-jackson3

git clone https://github.com/blogcin/jobrunr.git
cd jobrunr
git checkout fix/donotretry-with-tests
./gradlew :tests:e2e-native-jackson3:nativeTest

Relevant log output

java.lang.IllegalArgumentException: Can not set boolean field org.jobrunr.jobs.states.FailedState.doNotRetry to java.lang.Integer
    at java.base/jdk.internal.reflect.UnsafeBooleanFieldAccessorImpl.set(UnsafeBooleanFieldAccessorImpl.java)
    at java.base/java.lang.reflect.Field.set(Field.java)
    at java.base/java.lang.invoke.MethodHandle.invokeExact(MethodHandle.java)
    at tools.jackson.databind.deser.impl.FieldProperty.set(FieldProperty.java)
    at tools.jackson.databind.deser.impl.FieldProperty.deserializeAndSet(FieldProperty.java)
    at tools.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions