Repository: kestra-io/kestra (java)

Issue:
Behaviour regarding empty objects with `| json` filter and `json()` function
### Issue description

Kestra handles JSON objects differently when it comes to variables and inline defined objects that contain null values or objects which elements are null. Ideally, it should retain even those keys that are null valued - even though, sometimes it might be useful to drop such keys with null values, but that should be with an additional filter.

Best to describe is with a sample flow:
```
id: json
namespace: kestra.test

variables:
  object:
    key: value
    'null': null
    empty_object: {}
    object_with_empty_values:
      key1: null
      key2: null
    partly_empty_object:
      key1: value
      key2: 
    filled_object:
      key1: value1
      key2: value2
  

tasks:
  - id: vars
    type: io.kestra.core.tasks.log.Log
    message: |
      JSON filter: {{ vars.object | json }}
      JSON function: {{ json(vars.object | json) }}

  - id: local
    type: io.kestra.core.tasks.log.Log
    message: |
      JSON filter: {{ 
        {
          "key":"value", 
          "null": null, 
          "empty_object": {}, 
          "object_with_empty_values": 
            {
              "key1":null, 
              "key2":null
            }, 
          "partly_empty_object": 
            {
              "key1": "value", 
              "key2": null
            }, 
          "filled_object": 
            {
              "key1": "value1", 
              "key2": "value2"
            }
        } | json 
      }}
      JSON function: {{ 
        json('
        {
          "key":"value", 
          "null": null, 
          "empty_object": {}, 
          "object_with_empty_values": 
            {
              "key1":null, 
              "key2":null
            }, 
          "partly_empty_object": 
            {
              "key1": "value", 
              "key2": null
            }, 
          "filled_object": 
            {
              "key1": "value1", 
              "key2": "value2"
            }
        } 
        ')
      }}
```

Which results in:
### vars
```
JSON filter: {"filled_object":{"key1":"value1","key2":"value2"},"partly_empty_object":{"key1":"value"},"key":"value"}
JSON function: {"filled_object":{"key1":"value1","key2":"value2"},"partly_empty_object":{"key1":"value"},"key":"value"}
```

### local
```
JSON filter: {"empty_object":{},"object_with_empty_values":{"key1":null,"key2":null},"filled_object":{"key1":"value1","key2":"value2"},"partly_empty_object":{"key1":"value","key2":null},"null":null,"key":"value"}
JSON function: {"key":"value","partly_empty_object":{"key1":"value"},"filled_object":{"key1":"value1","key2":"value2"}}
```

Is there any way to handle (json) objects in such a way that the desired result would always be?
```{"empty_object":{},"object_with_empty_values":{"key1":null,"key2":null},"filled_object":{"key1":"value1","key2":"value2"},"partly_empty_object":{"key1":"value","key2":null},"null":null,"key":"value"}```

Pull request:
fix(core): Pebble JsonFunction and JsonFiter should use the stanard O…
…bjectMapper

Fixes #3211

Patch:
diff --git a/core/src/main/java/io/kestra/core/runners/pebble/filters/JsonFilter.java b/core/src/main/java/io/kestra/core/runners/pebble/filters/JsonFilter.java
index a11370172..79be1fe4f 100644
--- a/core/src/main/java/io/kestra/core/runners/pebble/filters/JsonFilter.java
+++ b/core/src/main/java/io/kestra/core/runners/pebble/filters/JsonFilter.java
@@ -2,11 +2,7 @@ package io.kestra.core.runners.pebble.filters;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.datatype.guava.GuavaModule;
-import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
-import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
-import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
+import io.kestra.core.serializers.JacksonMapper;
 import io.pebbletemplates.pebble.error.PebbleException;
 import io.pebbletemplates.pebble.extension.Filter;
 import io.pebbletemplates.pebble.template.EvaluationContext;
@@ -16,12 +12,7 @@ import java.util.List;
 import java.util.Map;
 
 public class JsonFilter implements Filter {
-    private static final ObjectMapper MAPPER = new ObjectMapper()
-        .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
-        .registerModule(new JavaTimeModule())
-        .registerModule(new Jdk8Module())
-        .registerModule(new ParameterNamesModule())
-        .registerModules(new GuavaModule());
+    private static final ObjectMapper MAPPER = JacksonMapper.ofJson();
 
     @Override
     public List<String> getArgumentNames() {
diff --git a/core/src/main/java/io/kestra/core/runners/pebble/functions/JsonFunction.java b/core/src/main/java/io/kestra/core/runners/pebble/functions/JsonFunction.java
index 3ba2b0752..527030006 100644
--- a/core/src/main/java/io/kestra/core/runners/pebble/functions/JsonFunction.java
+++ b/core/src/main/java/io/kestra/core/runners/pebble/functions/JsonFunction.java
@@ -1,20 +1,18 @@
 package io.kestra.core.runners.pebble.functions;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import io.kestra.core.serializers.JacksonMapper;
 import io.pebbletemplates.pebble.error.PebbleException;
 import io.pebbletemplates.pebble.extension.Function;
 import io.pebbletemplates.pebble.template.EvaluationContext;
 import io.pebbletemplates.pebble.template.PebbleTemplate;
-import io.kestra.core.runners.pebble.AbstractDate;
 
 import java.util.List;
 import java.util.Map;
 
 public class JsonFunction implements Function {
-    final static ObjectMapper MAPPER = new ObjectMapper();
-    private static final TypeReference<Object> TYPE_REFERENCE = new TypeReference<>() {};
+    private static final ObjectMapper MAPPER = JacksonMapper.ofJson();
 
     public List<String> getArgumentNames() {
         return List.of("json");
@@ -33,7 +31,7 @@ public class JsonFunction implements Function {
         String json = (String) args.get("json");;
 
         try {
-            return MAPPER.readValue(json, TYPE_REFERENCE);
+            return MAPPER.readValue(json, JacksonMapper.OBJECT_TYPE_REFERENCE);
         } catch (JsonProcessingException e) {
             throw new PebbleException(null, "Invalid json: " + e.getMessage(), lineNumber, self.getName());
         }
diff --git a/core/src/main/java/io/kestra/core/serializers/JacksonMapper.java b/core/src/main/java/io/kestra/core/serializers/JacksonMapper.java
index b956509fc..fdad2e3f2 100644
--- a/core/src/main/java/io/kestra/core/serializers/JacksonMapper.java
+++ b/core/src/main/java/io/kestra/core/serializers/JacksonMapper.java
@@ -23,9 +23,7 @@ import com.fasterxml.jackson.datatype.guava.GuavaModule;
 import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
-import io.kestra.core.plugins.DefaultPluginRegistry;
 import io.kestra.core.plugins.PluginModule;
-import io.kestra.core.plugins.serdes.PluginDeserializer;
 import io.kestra.core.serializers.ion.IonFactory;
 import io.kestra.core.serializers.ion.IonModule;
 import org.yaml.snakeyaml.LoaderOptions;
@@ -37,10 +35,8 @@ import java.util.TimeZone;
 
 public final class JacksonMapper {
     public static final TypeReference<Map<String, Object>> MAP_TYPE_REFERENCE = new TypeReference<>() {};
-
-    private static final TypeReference<List<Object>> LIST_TYPE_REFERENCE = new TypeReference<>() {};
-
-    private static final TypeReference<Object> OBJECT_TYPE_REFERENCE = new TypeReference<>() {};
+    public static final TypeReference<List<Object>> LIST_TYPE_REFERENCE = new TypeReference<>() {};
+    public static final TypeReference<Object> OBJECT_TYPE_REFERENCE = new TypeReference<>() {};
 
     private JacksonMapper() {}
 


Test patch:
diff --git a/core/src/test/java/io/kestra/core/runners/pebble/filters/JsonFilterTest.java b/core/src/test/java/io/kestra/core/runners/pebble/filters/JsonFilterTest.java
index 8607827e6..167029a54 100644
--- a/core/src/test/java/io/kestra/core/runners/pebble/filters/JsonFilterTest.java
+++ b/core/src/test/java/io/kestra/core/runners/pebble/filters/JsonFilterTest.java
@@ -71,5 +71,8 @@ class JsonFilterTest {
         assertThat(render, containsString("\"string\":\"string\""));
         assertThat(render, startsWith("{"));
         assertThat(render, endsWith("}"));
+
+        render = variableRenderer.render("{{ {\"empty_object\":{}} | json }}", Map.of());
+        assertThat(render, is("{\"empty_object\":{}}"));
     }
 }
diff --git a/core/src/test/java/io/kestra/core/runners/pebble/functions/JsonFunctionTest.java b/core/src/test/java/io/kestra/core/runners/pebble/functions/JsonFunctionTest.java
index 28f443ad2..89066275c 100644
--- a/core/src/test/java/io/kestra/core/runners/pebble/functions/JsonFunctionTest.java
+++ b/core/src/test/java/io/kestra/core/runners/pebble/functions/JsonFunctionTest.java
@@ -26,5 +26,8 @@ class JsonFunctionTest {
 
         render = variableRenderer.render("{{ json('[1, 2, 3]')[0] }}", Map.of());
         assertThat(render, is("1"));
+
+        render = variableRenderer.render("{{ json('{\"empty_object\":{}}') }}", Map.of());
+        assertThat(render, is("{\"empty_object\":{}}"));
     }
 }