Skip to content

Fix static ObjectMapper initialization trap in JsonUtils and add native CloudEvents Jackson support #1275

Description

@ricardozanini

What happened:
There are two related issues in the SDK's Jackson serialization layer that prevent proper integration with DI frameworks (like Quarkus or Spring) and cause native CloudEvent filtering/evaluation to fail.

Issue 1: Static ObjectMapper Trap in JsonUtils
io.serverlessworkflow.impl.jackson.JsonUtils caches the ObjectMapper in a static field at class load time:

private static ObjectMapper mapper = ObjectMapperFactoryProvider.instance().get().get();

If a framework attempts to inject a custom ObjectMapper via ObjectMapperFactoryProvider.instance().setFactory(...) after the JsonUtils class has been loaded, the factory override is completely ignored. This forces developers to use reflection to overwrite the static field.

Issue 2: Missing CloudEvent Serialization Support
The SDK natively exposes io.cloudevents.CloudEvent in its DSL (e.g., FuncEventFilterSpec). However, the default ObjectMapper fallback in ObjectMapperFactoryProvider does not register the CloudEvents Jackson module. When the engine evaluates event predicates (converting CloudEvent to JsonNode and back), Jackson throws either:

  • InvalidDefinitionException (Cannot construct instance of interface io.cloudevents.CloudEvent)
  • MismatchedInputException (Missing mandatory specversion attribute due to default POJO serializers mangling the CE envelope).

Proposed Solution

Remove static caching: Update JsonUtils to fetch the mapper dynamically:

public static ObjectMapper mapper() {
    return ObjectMapperFactoryProvider.instance().get().get();
}

Optimize fallback & register CE natively: To prevent performance drops from creating a new ObjectMapper() on every call, update ObjectMapperFactoryProvider to provide a static, pre-configured singleton as the default fallback. This default mapper must include the official CloudEvents module:

private static final ObjectMapper DEFAULT_MAPPER = new ObjectMapper()
        .findAndRegisterModules()
        .registerModule(io.cloudevents.jackson.JsonFormat.getCloudEventJacksonModule());

Native Converters for CloudEvent Types: Update JsonUtils to explicitly intercept CloudEvent and CloudEventData types, bypassing Jackson POJO serialization in favor of the official EventFormatProvider.

  • In fromValue(Object value):
} else if (value instanceof CloudEvent ce) {
    byte[] ceBytes = EventFormatProvider.getInstance().resolveFormat(JsonFormat.CONTENT_TYPE).serialize(ce);
    return mapper().readTree(ceBytes);
}
  • In convertValue(JsonNode jsonNode, Class<T> returnType):
} else if (CloudEvent.class.isAssignableFrom(returnType)) {
    byte[] ceBytes = mapper().writeValueAsBytes(jsonNode);
    obj = EventFormatProvider.getInstance().resolveFormat(JsonFormat.CONTENT_TYPE).deserialize(ceBytes);
}

Anything else we need to know?:
This was something I should've tested when implementing the event filtering DSL.

Environment:

  • Specification version used:

Activity

  1. ricardozanini commented on Mar 27, 2026

    @ricardozanini
    CollaboratorAuthor

    @fjtirado I remember we have some sort of support for CE already in the messaging code base, but I don't remember what we did in the models. I'll explore a bit and send a PR if necessary.

  2. ricardozanini commented on Mar 27, 2026

    @ricardozanini
    CollaboratorAuthor

    I have the fix already, I'm running more tests and will send the PR on Monday.

    I mistakenly created this:

      public FuncEventFilterPropertiesBuilder envelope(Predicate<CloudEvent> predicate) {
        this.eventProperties.setData(
            new EventDataPredicate().withPredicate(predicate, CloudEvent.class));
        return this;
      }
    
      public FuncEventFilterPropertiesBuilder envelope(ContextPredicate<CloudEvent> predicate) {
        this.eventProperties.setData(
            new EventDataPredicate().withPredicate(predicate, CloudEvent.class));
        return this;
      }
    
      public FuncEventFilterPropertiesBuilder envelope(FilterPredicate<CloudEvent> predicate) {
        this.eventProperties.setData(
            new EventDataPredicate().withPredicate(predicate, CloudEvent.class));
        return this;
      }

    The DefaultCloudEventPredicate is not expecting a CloudEvent predicate, so it's blowing everywhere. I'll add a new type to EventProperties to deal with this new predicate in the additionalProperties map. So then we can have an envelopeFilter in the predicate and test this correctly, without breaking backwards compatibility with the data predicates we already have.

  3. fjtirado commented on Mar 30, 2026

    @fjtirado
    Collaborator

    I have the fix already, I'm running more tests and will send the PR on Monday.

    I mistakenly created this:

    public FuncEventFilterPropertiesBuilder envelope(Predicate predicate) {
    this.eventProperties.setData(
    new EventDataPredicate().withPredicate(predicate, CloudEvent.class));
    return this;
    }

    public FuncEventFilterPropertiesBuilder envelope(ContextPredicate predicate) {
    this.eventProperties.setData(
    new EventDataPredicate().withPredicate(predicate, CloudEvent.class));
    return this;
    }

    public FuncEventFilterPropertiesBuilder envelope(FilterPredicate predicate) {
    this.eventProperties.setData(
    new EventDataPredicate().withPredicate(predicate, CloudEvent.class));
    return this;
    }
    The DefaultCloudEventPredicate is not expecting a CloudEvent predicate, so it's blowing everywhere. I'll add a new type to EventProperties to deal with this new predicate in the additionalProperties map. So then we can have an envelopeFilter in the predicate and test this correctly, without breaking backwards compatibility with the data predicates we already have.

    Hmmm, withPredicate sets the object field of EventData to a Predicate, later on, when EventData is converted to a Filter, getObject is used and that should call this method, which properly handle the Predicate object , so I do not think the envelopeFilter is needed. Can I see the exact failure we are trying to fix?, I suspect JavaExpressionFactory is not getting called, maybe because the priorities are not correctly set.

  4. added a commit that references this issue on Apr 1, 2026
    42e89a8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

javaPull requests that update java codespec:1.0.0

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions