Repository: kestra-io/kestra
Issue: Empty inputs are not added to the list of inputs
Labels: backend

### Issue description

As example, see the followning flow:

```yaml
id: trial
namespace: iss.trial
inputs:
- name: givenName
  type: STRING
  required: false
tasks:
- id: if
  type: io.kestra.core.tasks.flows.If
  condition: "{{inputs.givenName}}"
  then:
    - id: when-true
      type: io.kestra.core.tasks.log.Log
      message: 'Condition was true'
  else:
    - id: when-false
      type: io.kestra.core.tasks.log.Log
      message: 'Condition was false'
```

It didn't work as if the `givenName` input is not set, it will not be added to the list of inputs so the `inputs` variable will be missing (or if other inputs exist, the `inputs.givenName` variable will be missing). 

To make it work we must use the coalescing operator so the condition becomes:
```yaml
condition: "{{inputs.givenName ?? false}}"
```

This can be surprising and more complex than needed.

Maybe we can add non-set inputs in the list of inputs with a null value.


Issue comments:
From my POV, I would make declared variable null, because in this example :
* inputs.givenName > Is defined but has no value > `null`
* inputs.notDefined > Is not defined> `undefined`
as a user, I'd expect that input is set to `null` when `defaults` is not provided 
relate to #1515, we removed null values in the serialization process, that is, i must admit, one of the worst choice I've made so far :cry: 
@tchiotludo I think for inputs we can do something as it's an object with name/type/value so we can still serialize the object even when the value is null. There should be null check somewhere that remove inputs with null value.

Inputs with null values are filtered here for ex (there can be multiple places to update): https://github.com/kestra-io/kestra/blob/develop/core/src/main/java/io/kestra/core/runners/RunnerUtils.java#L120

Pull request review comments:

Patch that resolved the issue:
diff --git a/core/src/main/java/io/kestra/core/models/executions/Execution.java b/core/src/main/java/io/kestra/core/models/executions/Execution.java
index fecb1ba777..f1a933a55c 100644
--- a/core/src/main/java/io/kestra/core/models/executions/Execution.java
+++ b/core/src/main/java/io/kestra/core/models/executions/Execution.java
@@ -4,6 +4,7 @@
 import ch.qos.logback.classic.spi.LoggingEvent;
 import ch.qos.logback.classic.spi.ThrowableProxy;
 import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Streams;
 import lombok.Builder;
@@ -48,6 +49,7 @@ public class Execution implements DeletedInterface {
     List<TaskRun> taskRunList;
 
     @With
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
     Map<String, Object> inputs;
 
     @With
diff --git a/core/src/main/java/io/kestra/core/runners/RunnerUtils.java b/core/src/main/java/io/kestra/core/runners/RunnerUtils.java
index 521e857edb..1aa9ba91b0 100644
--- a/core/src/main/java/io/kestra/core/runners/RunnerUtils.java
+++ b/core/src/main/java/io/kestra/core/runners/RunnerUtils.java
@@ -117,7 +117,10 @@ public Map<String, Object> typedInputs(Flow flow, Execution execution, Map<Strin
                 }
 
                 if (!input.getRequired() && current == null) {
-                    return Optional.empty();
+                    return Optional.of(new AbstractMap.SimpleEntry<>(
+                        input.getName(),
+                        null
+                    ));
                 }
 
                 var parsedInput = parseInput(flow, execution, input, current);
@@ -126,7 +129,7 @@ public Map<String, Object> typedInputs(Flow flow, Execution execution, Map<Strin
             })
             .filter(Optional::isPresent)
             .map(Optional::get)
-            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+            .collect(HashMap::new, (map, entry) -> map.put(entry.getKey(), entry.getValue()), Map::putAll);
 
         return handleNestedInputs(results);
     }


Test patch:
diff --git a/core/src/test/java/io/kestra/core/runners/InputsTest.java b/core/src/test/java/io/kestra/core/runners/InputsTest.java
index c1fa9ac5b4..d194f7a61e 100644
--- a/core/src/test/java/io/kestra/core/runners/InputsTest.java
+++ b/core/src/test/java/io/kestra/core/runners/InputsTest.java
@@ -85,6 +85,15 @@ void missingRequired() {
         });
     }
 
+    @Test
+    void nonRequiredNoDefaultNoValueIsNull() {
+        HashMap<String, String> inputsWithMissingOptionalInput = new HashMap<>(inputs);
+        inputsWithMissingOptionalInput.remove("bool");
+
+        assertThat(typedInputs(inputsWithMissingOptionalInput).containsKey("bool"), is(true));
+        assertThat(typedInputs(inputsWithMissingOptionalInput).get("bool"), nullValue());
+    }
+
     @SuppressWarnings("unchecked")
     @Test
     void allValidInputs() throws URISyntaxException, IOException {
diff --git a/webserver/src/test/java/io/kestra/webserver/controllers/ExecutionControllerTest.java b/webserver/src/test/java/io/kestra/webserver/controllers/ExecutionControllerTest.java
index ef3169eaf4..4cd172d702 100644
--- a/webserver/src/test/java/io/kestra/webserver/controllers/ExecutionControllerTest.java
+++ b/webserver/src/test/java/io/kestra/webserver/controllers/ExecutionControllerTest.java
@@ -129,6 +129,8 @@ void trigger() {
         assertThat(result.getInputs().get("float"), is(42.42));
         assertThat(result.getInputs().get("file").toString(), startsWith("kestra:///io/kestra/tests/inputs/executions/"));
         assertThat(result.getInputs().get("file").toString(), startsWith("kestra:///io/kestra/tests/inputs/executions/"));
+        assertThat(result.getInputs().containsKey("bool"), is(true));
+        assertThat(result.getInputs().get("bool"), nullValue());
         assertThat(result.getLabels().get("a"), is("label-1"));
         assertThat(result.getLabels().get("b"), is("label-2"));
         assertThat(result.getLabels().get("flow-label-1"), is("flow-label-1"));