Skip to content

Commit 7466384

Browse files
authored
Replace JSONata4Java with dashjoin/jsonata-java for full JSONata compatibility (#69)
JSONata4Java is not a complete implementation of the JSONata spec, causing expressions that work on try.jsonata.org to fail in Kestra (e.g. nested array path expressions). Switch to dashjoin/jsonata-java which is a 1:1 port of the JavaScript reference implementation with 100% feature parity. Fixes #40 Co-authored-by: François Delbrayelle <fdelbrayelle@kestra.io>
1 parent a94d9c0 commit 7466384

3 files changed

Lines changed: 83 additions & 18 deletions

File tree

plugin-transform-json/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ jar {
1313
}
1414

1515
dependencies {
16-
implementation 'com.ibm.jsonata4java:JSONata4Java:2.6.2'
16+
implementation 'com.dashjoin:jsonata:0.9.9'
1717
}
Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
package io.kestra.plugin.transform.jsonata;
22

3-
import com.api.jsonata4java.expressions.EvaluateException;
4-
import com.api.jsonata4java.expressions.Expressions;
5-
import com.api.jsonata4java.expressions.ParseException;
3+
import static com.dashjoin.jsonata.Jsonata.jsonata;
4+
5+
import com.dashjoin.jsonata.JException;
6+
import com.dashjoin.jsonata.Jsonata;
67
import com.fasterxml.jackson.databind.JsonNode;
8+
import com.fasterxml.jackson.databind.ObjectMapper;
9+
import com.fasterxml.jackson.databind.node.NullNode;
710
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
811
import io.kestra.core.models.property.Property;
912
import io.kestra.core.models.tasks.Output;
1013
import io.kestra.core.models.tasks.RunnableTask;
1114
import io.kestra.core.models.tasks.Task;
1215
import io.kestra.core.runners.RunContext;
16+
import io.kestra.core.serializers.JacksonMapper;
1317
import io.swagger.v3.oas.annotations.media.Schema;
1418
import jakarta.validation.constraints.NotNull;
1519
import lombok.*;
1620
import lombok.experimental.SuperBuilder;
1721

18-
import java.io.IOException;
1922
import java.time.Duration;
2023

2124
import io.kestra.core.models.enums.MonacoLanguages;
@@ -28,36 +31,44 @@
2831
@NoArgsConstructor
2932
public abstract class Transform<T extends Output> extends Task implements JSONataInterface, RunnableTask<T> {
3033

34+
private static final ObjectMapper MAPPER = JacksonMapper.ofJson();
35+
3136
@PluginProperty(language = MonacoLanguages.JAVASCRIPT)
3237
private Property<String> expression;
3338

3439
@Builder.Default
3540
private Property<Integer> maxDepth = Property.ofValue(1000);
3641

3742
@Getter(AccessLevel.PRIVATE)
38-
private Expressions expressions;
43+
private Jsonata parsedExpression;
3944

4045
public void init(RunContext runContext) throws Exception {
41-
this.expressions = parseExpression(runContext);
46+
var exprString = runContext.render(this.expression).as(String.class).orElseThrow();
47+
try {
48+
this.parsedExpression = jsonata(exprString);
49+
} catch (JException e) {
50+
throw new IllegalArgumentException("Invalid JSONata expression. Error: " + e.getMessage(), e);
51+
}
4252
}
4353

4454
protected JsonNode evaluateExpression(RunContext runContext, JsonNode jsonNode) {
4555
try {
46-
long timeoutInMilli = runContext.render(getTimeout()).as(Duration.class)
56+
var timeoutInMilli = runContext.render(getTimeout()).as(Duration.class)
4757
.map(Duration::toMillis)
4858
.orElse(Long.MAX_VALUE);
59+
var rMaxDepth = runContext.render(getMaxDepth()).as(Integer.class).orElseThrow();
4960

50-
return this.expressions.evaluate(jsonNode, timeoutInMilli, runContext.render(getMaxDepth()).as(Integer.class).orElseThrow());
51-
} catch (EvaluateException | IllegalVariableEvaluationException e) {
52-
throw new RuntimeException("Failed to evaluate expression", e);
53-
}
54-
}
61+
var data = MAPPER.convertValue(jsonNode, Object.class);
62+
var frame = this.parsedExpression.createFrame();
63+
frame.setRuntimeBounds(timeoutInMilli, rMaxDepth);
5564

56-
private Expressions parseExpression(RunContext runContext) throws IllegalVariableEvaluationException {
57-
try {
58-
return Expressions.parse(runContext.render(this.expression).as(String.class).orElseThrow());
59-
} catch (ParseException | IOException e) {
60-
throw new IllegalArgumentException("Invalid JSONata expression. Error: " + e.getMessage(), e);
65+
var result = this.parsedExpression.evaluate(data, frame);
66+
if (result == null) {
67+
return NullNode.getInstance();
68+
}
69+
return MAPPER.valueToTree(result);
70+
} catch (JException | IllegalVariableEvaluationException e) {
71+
throw new RuntimeException("Failed to evaluate expression", e);
6172
}
6273
}
6374
}

plugin-transform-json/src/test/java/io/kestra/plugin/transform/jsonata/TransformValueTest.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.kestra.plugin.transform.jsonata;
22

3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
35
import io.kestra.core.junit.annotations.KestraTest;
46
import io.kestra.core.models.property.Property;
57
import io.kestra.core.runners.RunContext;
@@ -8,6 +10,8 @@
810
import org.junit.jupiter.api.Assertions;
911
import org.junit.jupiter.api.Test;
1012

13+
import static org.assertj.core.api.Assertions.assertThat;
14+
1115
@KestraTest
1216
class TransformValueTest {
1317

@@ -79,4 +83,54 @@ void shouldGetOutputForValidExprReturningObjectForFromJSON() throws Exception {
7983
Assertions.assertNotNull(output);
8084
Assertions.assertEquals("{\"order_id\":\"ABC123\",\"customer_name\":\"John Doe\",\"total_price\":4.2}", output.getValue().toString());
8185
}
86+
87+
@Test
88+
void shouldHandleNestedArrayExpressionFromIssue40() throws Exception {
89+
String input = """
90+
{
91+
"filterTuples": [
92+
{
93+
"filter": [
94+
{"parent": {"parent": {"hybrisId": "8796977876513"}, "hybrisId": "8796995440161"}, "hybrisId": "8796998946337"},
95+
{"parent": {"parent": {"hybrisId": "8796977876513"}, "hybrisId": "8796995472929"}, "hybrisId": "8797002583585"},
96+
{"parent": {"parent": {"hybrisId": "8796977843745"}, "hybrisId": "8796995341857"}, "hybrisId": "8796999798305"}
97+
]
98+
},
99+
{
100+
"filter": [
101+
{"parent": {"parent": {"hybrisId": "8796977876513"}, "hybrisId": "8796995440161"}, "hybrisId": "8796998946337"},
102+
{"parent": {"parent": {"hybrisId": "8796977876513"}, "hybrisId": "8796995472929"}, "hybrisId": "8797002583585"},
103+
{"parent": {"parent": {"hybrisId": "8796977843745"}, "hybrisId": "8796995341857"}, "hybrisId": "8796999765537"}
104+
]
105+
}
106+
]
107+
}
108+
""";
109+
var expression = "[filterTuples.[filter.(parent.parent.hybrisId & \"/\" & parent.hybrisId & \"/\" & hybrisId)]]";
110+
111+
var task = TransformValue.builder()
112+
.id("test")
113+
.type(TransformValue.class.getName())
114+
.from(Property.of(input))
115+
.expression(Property.of(expression))
116+
.build();
117+
118+
var runContext = runContextFactory.of();
119+
var output = task.run(runContext);
120+
121+
// Verify it's a nested array (not flattened)
122+
assertThat(output.getValue()).isNotNull();
123+
// The result should be a JSON array of arrays
124+
var mapper = new ObjectMapper();
125+
JsonNode result = mapper.valueToTree(output.getValue());
126+
assertThat(result.isArray()).isTrue();
127+
assertThat(result.size()).isEqualTo(2);
128+
assertThat(result.get(0).isArray()).isTrue();
129+
assertThat(result.get(0).size()).isEqualTo(3);
130+
assertThat(result.get(0).get(0).asText()).isEqualTo("8796977876513/8796995440161/8796998946337");
131+
assertThat(result.get(0).get(1).asText()).isEqualTo("8796977876513/8796995472929/8797002583585");
132+
assertThat(result.get(0).get(2).asText()).isEqualTo("8796977843745/8796995341857/8796999798305");
133+
assertThat(result.get(1).isArray()).isTrue();
134+
assertThat(result.get(1).get(2).asText()).isEqualTo("8796977843745/8796995341857/8796999765537");
135+
}
82136
}

0 commit comments

Comments
 (0)