Skip to content

Commit 0213e73

Browse files
committed
fix(core): preserve null in map rendering for Pebble templates (#15783)
When a Pebble expression evaluates to null (e.g. {{ inputs.x ?? null }}), JsonWriter now correctly detects no output was produced. For map rendering (trigger inputs, subflow inputs), null values are preserved in the map so downstream FlowInputOutput treats them as "not provided". For string rendering (render(String) public API), null is still converted to "" for backward compatibility. This fixes Flow trigger inputs passing "" instead of null to child flows, which caused DateTimeParseException for optional DATETIME inputs.
1 parent 9f5e4b9 commit 0213e73

5 files changed

Lines changed: 79 additions & 21 deletions

File tree

core/src/main/java/io/kestra/core/runners/DefaultRunContext.java

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import java.util.*;
1111
import java.util.concurrent.atomic.AtomicBoolean;
1212
import java.util.function.Consumer;
13-
import java.util.stream.Collectors;
1413

1514
import org.apache.commons.lang3.RandomStringUtils;
1615
import org.slf4j.Logger;
@@ -43,7 +42,6 @@
4342
import lombok.With;
4443

4544
import static io.kestra.core.utils.MapUtils.mergeWithNullableValues;
46-
import static io.kestra.core.utils.Rethrow.throwFunction;
4745

4846
/**
4947
* Default and mutable implementation of {@link RunContext}.
@@ -322,18 +320,18 @@ public Map<String, String> renderMap(Map<String, String> inline, Map<String, Obj
322320
}
323321

324322
Map<String, Object> allVariables = mergeWithNullableValues(this.variables, decryptVariables(variables));
325-
return inline
326-
.entrySet()
327-
.stream()
328-
.map(
329-
throwFunction(
330-
entry -> new AbstractMap.SimpleEntry<>(
331-
this.render(entry.getKey(), allVariables),
332-
this.render(entry.getValue(), allVariables)
333-
)
334-
)
335-
)
336-
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
323+
Map<String, String> rendered = new LinkedHashMap<>();
324+
for (Map.Entry<String, String> entry : inline.entrySet()) {
325+
String renderedKey = this.render(entry.getKey(), allVariables);
326+
if (renderedKey == null) {
327+
throw new IllegalVariableEvaluationException("Unable to render map: key rendered to null");
328+
}
329+
String renderedValue = this.render(entry.getValue(), allVariables);
330+
if (renderedValue != null) {
331+
rendered.put(renderedKey, renderedValue);
332+
}
333+
}
334+
return rendered;
337335
}
338336

339337
@Override

core/src/main/java/io/kestra/core/runners/VariableRenderer.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,11 @@ public Object renderTyped(String inline, Map<String, Object> variables) throws I
6363
}
6464

6565
public String render(String inline, Map<String, Object> variables, boolean recursive) throws IllegalVariableEvaluationException {
66-
return (String) this.render(inline, variables, recursive, true);
66+
if (inline == null) {
67+
return null;
68+
}
69+
String result = (String) this.render(inline, variables, recursive, true);
70+
return result != null ? result : "";
6771
}
6872

6973
public Object render(Object inline, Map<String, Object> variables, boolean recursive, boolean stringify) throws IllegalVariableEvaluationException {
@@ -187,7 +191,7 @@ private Object renderRecursively(int renderingCount, Object inline, Map<String,
187191
}
188192

189193
Object result = this.renderOnce(inline, variables, stringify);
190-
if (result.equals(inline)) {
194+
if (result == null || Objects.equals(result, inline)) {
191195
return result;
192196
}
193197

@@ -203,7 +207,7 @@ public Map<String, Object> render(Map<String, Object> in, Map<String, Object> va
203207

204208
for (Map.Entry<String, Object> r : in.entrySet()) {
205209
String key = this.render(r.getKey(), variables);
206-
Object value = renderObject(r.getValue(), variables, recursive).orElse(r.getValue());
210+
Object value = renderObject(r.getValue(), variables, recursive).orElse(null);
207211

208212
map.putIfAbsent(
209213
key,
@@ -227,7 +231,7 @@ public Optional<Object> renderObject(Object object, Map<String, Object> variable
227231
} else if (object instanceof Set set) {
228232
return Optional.of(this.render(set, variables, recursive));
229233
} else if (object instanceof String string) {
230-
return Optional.of(this.render(string, variables, recursive));
234+
return Optional.ofNullable(this.render(string, variables, recursive, true));
231235
}
232236

233237
// Return the given object if it cannot be rendered.
@@ -242,7 +246,7 @@ public List<Object> renderList(List<Object> list, Map<String, Object> variables,
242246
List<Object> result = new ArrayList<>();
243247

244248
for (Object inline : list) {
245-
result.add(this.renderObject(inline, variables, recursive).orElse(inline));
249+
result.add(this.renderObject(inline, variables, recursive).orElse(null));
246250
}
247251

248252
return result;

core/src/main/java/io/kestra/core/runners/pebble/JsonWriter.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,50 +16,65 @@ public class JsonWriter extends OutputWriter implements SpecializedWriter {
1616
private static final ObjectMapper MAPPER = JacksonMapper.ofJson();
1717

1818
private final StringWriter stringWriter = new StringWriter();
19+
private boolean hasOutput = false;
1920

2021
@Override
2122
public void writeSpecialized(int i) {
23+
hasOutput = true;
2224
stringWriter.getBuffer().append(i);
2325
}
2426

2527
@Override
2628
public void writeSpecialized(long l) {
29+
hasOutput = true;
2730
stringWriter.getBuffer().append(l);
2831
}
2932

3033
@Override
3134
public void writeSpecialized(double d) {
35+
hasOutput = true;
3236
stringWriter.getBuffer().append(d);
3337
}
3438

3539
@Override
3640
public void writeSpecialized(float f) {
41+
hasOutput = true;
3742
stringWriter.getBuffer().append(f);
3843
}
3944

4045
@Override
4146
public void writeSpecialized(short s) {
47+
hasOutput = true;
4248
stringWriter.getBuffer().append(s);
4349
}
4450

4551
@Override
4652
public void writeSpecialized(byte b) {
53+
hasOutput = true;
4754
stringWriter.getBuffer().append(b);
4855
}
4956

5057
@Override
5158
public void writeSpecialized(char c) {
59+
hasOutput = true;
5260
stringWriter.getBuffer().append(c);
5361
}
5462

5563
@Override
5664
public void writeSpecialized(String s) {
65+
if (s == null) {
66+
return;
67+
}
68+
hasOutput = true;
5769
stringWriter.getBuffer().append(s);
5870
}
5971

6072
@SneakyThrows
6173
@Override
6274
public void write(Object o) {
75+
if (o == null) {
76+
return;
77+
}
6378
if (o instanceof Map) {
6479
writeSpecialized(MAPPER.writeValueAsString(o));
6580
} else if (o instanceof Collection) {
@@ -73,6 +88,9 @@ public void write(Object o) {
7388

7489
@Override
7590
public void write(char[] cbuf, int off, int len) throws IOException {
91+
if (len > 0) {
92+
hasOutput = true;
93+
}
7694
this.stringWriter.write(cbuf, off, len);
7795
}
7896

@@ -93,6 +111,6 @@ public String toString() {
93111

94112
@Override
95113
public Object output() {
96-
return this.toString();
114+
return hasOutput ? this.toString() : null;
97115
}
98116
}

core/src/test/java/io/kestra/core/runners/VariableRendererTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,44 @@ void shouldKeepKeyOrderWhenRenderingMap() throws IllegalVariableEvaluationExcept
102102
assertThat(result_value3.keySet()).containsExactly("bar-1", "bar-2", "bar-3");
103103
}
104104

105+
@Test
106+
void shouldRenderStringAsEmptyForNull() throws IllegalVariableEvaluationException {
107+
assertThat(variableRenderer.render("{{ null }}", Map.of())).isEmpty();
108+
assertThat(variableRenderer.render("{{ true ? null : 'work' }}", Map.of())).isEmpty();
109+
}
110+
111+
@Test
112+
void shouldRenderStringAsString() throws IllegalVariableEvaluationException {
113+
assertThat(variableRenderer.render("{{ false ? null : 'work' }}", Map.of())).isEqualTo("work");
114+
assertThat(variableRenderer.render("{{ 42 }}", Map.of())).isEqualTo("42");
115+
assertThat(variableRenderer.render("{{ true }}", Map.of())).isEqualTo("true");
116+
}
117+
118+
@Test
119+
void shouldRenderStringAsEmptyForNullRecursively() throws IllegalVariableEvaluationException {
120+
assertThat(variableRenderer.render("{{ null }}", Map.of(), true)).isEmpty();
121+
assertThat(variableRenderer.render("prefix {{ null }}", Map.of(), true)).isEqualTo("prefix ");
122+
}
123+
124+
@Test
125+
void shouldRenderStringWithExplicitEmptyOutput() throws IllegalVariableEvaluationException {
126+
assertThat(variableRenderer.render("{{ '' }}", Map.of())).isEqualTo("");
127+
assertThat(variableRenderer.render("prefix {{ null }}", Map.of())).isEqualTo("prefix ");
128+
}
129+
130+
@Test
131+
void shouldRenderMapWithNullValues() throws IllegalVariableEvaluationException {
132+
Map<String, Object> input = new LinkedHashMap<>();
133+
input.put("key1", "{{ null }}");
134+
input.put("key2", "{{ 'hello' }}");
135+
input.put("key3", "static");
136+
Map<String, Object> result = variableRenderer.render(input, Map.of());
137+
assertThat(result.get("key1")).isNull();
138+
assertThat(result.get("key2")).isEqualTo("hello");
139+
assertThat(result.get("key3")).isEqualTo("static");
140+
assertThat(result).containsKey("key1");
141+
}
142+
105143
public static class TestVariableRenderer extends VariableRenderer {
106144

107145
public TestVariableRenderer(ApplicationContext applicationContext,

core/src/test/java/io/kestra/core/runners/pebble/PebbleVariableRendererTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ void out() throws IllegalVariableEvaluationException {
7474
assertThat((String) render.get("map")).startsWith("{");
7575
assertThat((String) render.get("map")).endsWith("}");
7676
assertThat((String) render.get("escape")).contains("[\"string\",1,1.123] // {");
77-
assertThat((String) render.get("empty")).isEqualTo("");
77+
assertThat(render.get("empty")).isNull();
7878
assertThat((String) render.get("concat")).isEqualTo("applepearbanana");
7979
}
8080

0 commit comments

Comments
 (0)