Skip to content

Commit cc95fc9

Browse files
committed
Support both Jackson 2 and Jackson 3
The Flowable REST Application contains both Jackson 2 and Jackson 3. However, the flowable engines have an optional dependency on Jackson 2. If Jackson 2 is needed it should be manually configured. All previous places that were using Jackson 2 for variable values. e.g. Task Candidates etc. are now supporting both Jackson 2 and Jackson 3 values. Storing Jackson 3 and Jackson 2 ObjectNode / ArrayNode variables is supported regardless of the preferred variable json mapper
1 parent 967f692 commit cc95fc9

File tree

71 files changed

+7280
-274
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+7280
-274
lines changed

distro/src/readme.html

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,53 @@ <h3>Release Notes - Flowable - 8.0.0</h3>
4040
Date properties e.g., Process Instance start time will be returned as an ISO 8601 in the UTC timezone.
4141
E.g., if the start time was returned as <code>2025-09-24T09:58:12.609+02:00</code> now it is returned as <code>2025-09-24T07:58:12.609Z</code>.
4242
</li>
43+
<li>
44+
Jackson 3 is used for Flowable internal json manipulation.
45+
There is still support for Jackson 2 variables.
46+
With Spring Boot this can be enabled by setting <code>flowable.variable-json-mapper</code> to <code>jackson2</code>.
47+
By default, Jackson 3 is being used for variables.
48+
When configuring Flowable without Spring Boot then the <code>variableJsonMapper</code> on the process, cmmn and app engine configurations
49+
should be set to be <code>org.flowable.common.engine.impl.json.jackson2.Jackson2VariableJsonMapper</code>
50+
and the <code>org.flowable.common.rest.variable.Jackson2JsonObjectRestVariableConverter</code> should be added to the appropriate rest response factories.
51+
</li>
52+
<li>
53+
Notable changes when using JSON in expressions / scripts:
54+
<ul>
55+
<li>Packages have changed from <code>com.fasterxml.jackson.databind</code> and <code>com.fasterxml.jackson.core</code> to <code>tools.jackson.databind</code> and <code>tools.jackson.core</code> </li>
56+
<li><code>elements()</code> no longer returns <code>Iterator&lt;JsonNode&gt;</code> and instead returns <code>Collection&lt;JsonNode&gt;</code></li>
57+
<li><code>values()</code> no longer returns <code>Iterator&lt;JsonNode&gt;</code> and instead returns <code>Collection&lt;JsonNode&gt;</code></li>
58+
<li><code>Iterator&lt;String&gt; fieldNames()</code> replaced by <code>Collection&lt;String&gt; propertyNames()</code></li>
59+
<li><code>Iterator&lt;Map.Entry&lt;String, JsonNode&gt;&gt; fields()</code> replaced by <code>Set&lt;Map.Entry&lt;String, JsonNode&gt;&gt; properties()</code></li>
60+
<li><code>JsonNode with(String)</code> replaced by <code>ObjectNode withObject(String)</code>, and if not using a pointer then it is better to use <code>ObjectNode withProperty(String)</code></li>
61+
<li><code>TextNode textNode(String)</code> replaced by <code>StringNode stringNode(String)</code></li>
62+
<li><code>List&lt;String&gt; findValuesAsText(String)</code> replaced by <code>List&lt;String&gt; findValuesAsString(String)</code></li>
63+
<li><code>List&lt;String&gt; findValuesAsText(String, List&lt;String&gt;)</code> replaced by <code>List&lt;String&gt; findValuesAsString(String, List&lt;String&gt;)</code></li>
64+
<li><code>boolean isContainerNode()</code> replaced by <code>boolean isContainer()</code></li>
65+
<li><code>boolean isEmpty(SerializerProvider)</code> replaced by <code>boolean isEmpty(SerializationContext)</code></li>
66+
<li><code>JsonNode put(String, JsonNode)</code> replaced by <code>JsonNode replace(String, JsonNode)</code></li>
67+
<li><code>JsonNode putAll(Map&lt;String, ? extends JsonNode&gt;)</code> replaced by <code>JsonNode setAll(Map&lt;String, ? extends JsonNode&gt;)</code></li>
68+
<li><code>JsonNode putAll(ObjectNode)</code> replaced by <code>JsonNode setAll(ObjectNode)></code></li>
69+
<li><code>JsonParser traverse()</code> removed without replacement</li>
70+
<li><code>JsonParser traverse(ObjectCodec)</code> replaced by <code>JsonParser traverse(ObjectReadContext)</code></li>
71+
<li><code>void serialize(JsonGenerator, SerializerProvider)</code> replaced by <code>void serialize(JsonGenerator, SerializationContext)</code></li>
72+
<li><code>void serializeWithType(JsonGenerator, SerializerProvider, TypeSerializer)</code> replaced by <code>void serializeWithType(JsonGenerator, SerializationContext, TypeSerializer)</code></li>
73+
</ul>
74+
Methods deprecated in Jackson 3:
75+
<ul>
76+
<li><code>String asText()</code> replaced by <code>String asString()</code></li>
77+
<li><code>String asText(String)</code> replaced by <code>String asString(String)</code></li>
78+
<li><code>boolean isTextual()</code> replaced by <code>boolean isString()</code></li>
79+
<li><code>String textValue()</code> replaced by <code>String stringValue()</code></li>
80+
</ul>
81+
There is also a behaviour change in the different <code>xxxValue</code> and <code>asXXX</code> methods.
82+
In Jackson 3 those methods would fail if the node is not of the appropriate type and / or if it cannot coerce the value to the requested type.
83+
e.g.
84+
<ul>
85+
<li>When using <code>stringValue</code> on a non <code>StringNode</code> or <code>NullNode</code> it would fail, in Jackson 2 it would return <code>null</code>.</li>
86+
<li>When using <code>asString</code> on a <code>ObjectNode</code> or <code>ArrayNode</code> it would fail, in Jackson 2 it would return an empty string.</li>
87+
</ul>
88+
89+
</li>
4390
</ul>
4491
<h3>Release Notes - Flowable - 7.2.0</h3>
4592

modules/flowable-app-engine/src/main/java/org/flowable/app/engine/AppEngineConfiguration.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@
6969
import org.flowable.common.engine.impl.el.ExpressionManager;
7070
import org.flowable.common.engine.impl.interceptor.CommandInterceptor;
7171
import org.flowable.common.engine.impl.interceptor.EngineConfigurationConstants;
72+
import org.flowable.common.engine.impl.json.VariableJsonMapper;
73+
import org.flowable.common.engine.impl.json.jackson3.Jackson3VariableJsonMapper;
7274
import org.flowable.common.engine.impl.persistence.deploy.DefaultDeploymentCache;
7375
import org.flowable.common.engine.impl.persistence.deploy.DeploymentCache;
7476
import org.flowable.common.engine.impl.persistence.entity.TableDataManager;
@@ -156,6 +158,7 @@ public class AppEngineConfiguration extends AbstractBuildableEngineConfiguration
156158
*/
157159
protected boolean jsonVariableTypeTrackObjects = true;
158160

161+
protected VariableJsonMapper variableJsonMapper;
159162

160163
protected BusinessCalendarManager businessCalendarManager;
161164

@@ -257,6 +260,14 @@ public void initMybatisTypeHandlers(Configuration configuration) {
257260
configuration.getTypeHandlerRegistry().register(VariableType.class, JdbcType.VARCHAR, new IbatisVariableTypeHandler(variableTypes));
258261
}
259262

263+
@Override
264+
public void initObjectMapper() {
265+
super.initObjectMapper();
266+
if (variableJsonMapper == null) {
267+
variableJsonMapper = new Jackson3VariableJsonMapper(objectMapper);
268+
}
269+
}
270+
260271
public void initExpressionManager() {
261272
if (expressionManager == null) {
262273
expressionManager = new DefaultExpressionManager(beans);
@@ -408,9 +419,9 @@ public void initVariableTypes() {
408419
variableTypes.addType(new BigDecimalType());
409420
variableTypes.addType(new BigIntegerType());
410421
variableTypes.addType(new UUIDType());
411-
variableTypes.addType(new JsonType(getMaxLengthString(), variableLengthVerifier, objectMapper, jsonVariableTypeTrackObjects));
422+
variableTypes.addType(new JsonType(getMaxLengthString(), variableLengthVerifier, variableJsonMapper, jsonVariableTypeTrackObjects));
412423
// longJsonType only needed for reading purposes
413-
variableTypes.addType(JsonType.longJsonType(getMaxLengthString(), variableLengthVerifier, objectMapper, jsonVariableTypeTrackObjects));
424+
variableTypes.addType(JsonType.longJsonType(getMaxLengthString(), variableLengthVerifier, variableJsonMapper, jsonVariableTypeTrackObjects));
414425
variableTypes.addType(new ByteArrayType(variableLengthVerifier));
415426
variableTypes.addType(new EmptyCollectionType());
416427
variableTypes.addType(new SerializableType(serializableVariableTypeTrackDeserializedObjects, variableLengthVerifier));
@@ -432,6 +443,7 @@ public void configureVariableServiceConfiguration() {
432443

433444
this.variableServiceConfiguration.setMaxLengthString(this.getMaxLengthString());
434445
this.variableServiceConfiguration.setSerializableVariableTypeTrackDeserializedObjects(this.isSerializableVariableTypeTrackDeserializedObjects());
446+
this.variableServiceConfiguration.setVariableJsonMapper(this.variableJsonMapper);
435447
}
436448

437449
public void initVariableServiceConfiguration() {
@@ -740,6 +752,17 @@ public AppEngineConfiguration setJsonVariableTypeTrackObjects(boolean jsonVariab
740752
return this;
741753
}
742754

755+
@Override
756+
public VariableJsonMapper getVariableJsonMapper() {
757+
return variableJsonMapper;
758+
}
759+
760+
@Override
761+
public AppEngineConfiguration setVariableJsonMapper(VariableJsonMapper variableJsonMapper) {
762+
this.variableJsonMapper = variableJsonMapper;
763+
return this;
764+
}
765+
743766
public boolean isDisableIdmEngine() {
744767
return disableIdmEngine;
745768
}

modules/flowable-app-rest/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,11 @@
756756
<artifactId>spring-boot-kafka</artifactId>
757757
</dependency>
758758

759+
<dependency>
760+
<groupId>org.springframework.boot</groupId>
761+
<artifactId>spring-boot-jackson2</artifactId>
762+
</dependency>
763+
759764
<dependency>
760765
<groupId>com.h2database</groupId>
761766
<artifactId>h2</artifactId>
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/* Licensed under the Apache License, Version 2.0 (the "License");
2+
* you may not use this file except in compliance with the License.
3+
* You may obtain a copy of the License at
4+
*
5+
* http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*/
13+
package org.flowable.rest.app.variable;
14+
15+
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
18+
import org.flowable.cmmn.api.CmmnRuntimeService;
19+
import org.flowable.cmmn.api.runtime.CaseInstance;
20+
import org.flowable.cmmn.engine.test.CmmnDeployment;
21+
import org.flowable.cmmn.spring.impl.test.FlowableCmmnSpringExtension;
22+
import org.flowable.engine.RuntimeService;
23+
import org.flowable.engine.runtime.ProcessInstance;
24+
import org.flowable.engine.test.Deployment;
25+
import org.flowable.spring.impl.test.FlowableSpringExtension;
26+
import org.junit.jupiter.api.Test;
27+
import org.junit.jupiter.api.extension.ExtendWith;
28+
import org.springframework.beans.factory.annotation.Autowired;
29+
import org.springframework.boot.resttestclient.TestRestTemplate;
30+
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate;
31+
import org.springframework.boot.test.context.SpringBootTest;
32+
import org.springframework.http.HttpStatus;
33+
import org.springframework.http.ResponseEntity;
34+
35+
import tools.jackson.databind.JsonNode;
36+
import tools.jackson.databind.ObjectMapper;
37+
import tools.jackson.databind.node.ArrayNode;
38+
import tools.jackson.databind.node.ObjectNode;
39+
40+
import net.javacrumbs.jsonunit.core.Option;
41+
42+
/**
43+
* @author Filip Hrisafov
44+
*/
45+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
46+
properties = {
47+
"flowable.variable-json-mapper=jackson2"
48+
})
49+
@ExtendWith({
50+
FlowableCmmnSpringExtension.class,
51+
FlowableSpringExtension.class,
52+
})
53+
@AutoConfigureTestRestTemplate
54+
public class RestJackson2JsonVariableTest {
55+
56+
@Autowired
57+
protected RuntimeService runtimeService;
58+
59+
@Autowired
60+
protected CmmnRuntimeService cmmnRuntimeService;
61+
62+
@Autowired
63+
protected TestRestTemplate restTemplate;
64+
65+
@Autowired
66+
protected ObjectMapper objectMapper;
67+
68+
@Test
69+
@Deployment(resources = "oneTaskProcess.bpmn20.xml")
70+
void createProcessWithJsonVariable() {
71+
ObjectNode request = objectMapper.createObjectNode()
72+
.put("processDefinitionKey", "oneTaskProcess")
73+
.put("returnVariables", true);
74+
ArrayNode variables = request.putArray("variables");
75+
variables.addObject()
76+
.put("name", "customer")
77+
.put("type", "json")
78+
.putObject("value")
79+
.put("name", "Kermit");
80+
ResponseEntity<JsonNode> response = restTemplate.withBasicAuth("rest-admin", "test")
81+
.postForEntity("/service/runtime/process-instances", request, JsonNode.class);
82+
83+
assertThat(response.getStatusCode()).as(response.toString()).isEqualTo(HttpStatus.CREATED);
84+
85+
assertThatJson(response.getBody())
86+
.when(Option.IGNORING_ARRAY_ORDER)
87+
.inPath("variables")
88+
.isEqualTo("""
89+
[
90+
{
91+
name: 'customer',
92+
type: 'json',
93+
value: {
94+
'name': 'Kermit'
95+
},
96+
scope: 'local'
97+
}
98+
]
99+
""");
100+
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
101+
.processDefinitionKey("oneTaskProcess")
102+
.includeProcessVariables()
103+
.singleResult();
104+
105+
assertThat(processInstance.getProcessVariables())
106+
.extractingByKey("customer")
107+
.isInstanceOf(com.fasterxml.jackson.databind.node.ObjectNode.class);
108+
}
109+
110+
@Test
111+
@CmmnDeployment(resources = "oneHumanTaskCase.cmmn")
112+
void createCaseWithJsonVariable() {
113+
ObjectNode request = objectMapper.createObjectNode()
114+
.put("caseDefinitionKey", "oneHumanTaskCase")
115+
.put("returnVariables", true);
116+
ArrayNode variables = request.putArray("variables");
117+
variables.addObject()
118+
.put("name", "customer")
119+
.put("type", "json")
120+
.putObject("value")
121+
.put("name", "Kermit");
122+
ResponseEntity<JsonNode> response = restTemplate.withBasicAuth("rest-admin", "test")
123+
.postForEntity("/cmmn-api/cmmn-runtime/case-instances", request, JsonNode.class);
124+
125+
assertThat(response.getStatusCode()).as(response.toString()).isEqualTo(HttpStatus.CREATED);
126+
127+
assertThatJson(response.getBody())
128+
.when(Option.IGNORING_ARRAY_ORDER)
129+
.inPath("variables")
130+
.isEqualTo("""
131+
[
132+
{
133+
name: 'customer',
134+
type: 'json',
135+
value: {
136+
'name': 'Kermit'
137+
},
138+
scope: 'local'
139+
}
140+
]
141+
""");
142+
143+
CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceQuery()
144+
.caseDefinitionKey("oneHumanTaskCase")
145+
.includeCaseVariables()
146+
.singleResult();
147+
148+
assertThat(caseInstance.getCaseVariables())
149+
.extractingByKey("customer")
150+
.isInstanceOf(com.fasterxml.jackson.databind.node.ObjectNode.class);
151+
}
152+
153+
}

0 commit comments

Comments
 (0)