Repository: kestra-io/kestra (java)

Issue:
Some deprecation notice doesn't appear in the embedded doc
### Describe the issue

For the io.kestra.plugin.scripts.python.Script task, the deprecation notice icon appear for some properties but not for all, (I.E : OutputDirectory), while the validator do tag the flow with a warn since it use a deprecated property.

![Image](https://github.com/user-attachments/assets/b50dc565-6747-4c3e-aebd-57dd7497d1ec)

### Environment

- Kestra Version: 0.21.5


Pull request description:
fix: doc and deprecated field was not showing for dynamic non-string properties
- failing test reproducing an issue where doc and deprecated field are not generated for dynamic non-string fields with default values: https://github.com/kestra-io/plugin-jdbc/issues/567

- [x] actual fix
- [ ] Loic: 'It must be backported to 0.22!'

Fixes #7842
Fixes [#567](https://github.com/kestra-io/plugin-jdbc/issues/567)

Patch:
diff --git a/core/src/main/java/io/kestra/core/docs/JsonSchemaGenerator.java b/core/src/main/java/io/kestra/core/docs/JsonSchemaGenerator.java
index a5af1278f..945adc2b0 100644
--- a/core/src/main/java/io/kestra/core/docs/JsonSchemaGenerator.java
+++ b/core/src/main/java/io/kestra/core/docs/JsonSchemaGenerator.java
@@ -81,7 +81,7 @@ public class JsonSchemaGenerator {
                 objectNode.put("type", "array");
             }
             replaceAnyOfWithOneOf(objectNode);
-            pullOfDefaultFromOneOf(objectNode);
+            pullDocumentationAndDefaultFromOneOf(objectNode);
             removeRequiredOnPropsWithDefaults(objectNode);
 
             return JacksonMapper.toMap(objectNode);
@@ -122,22 +122,35 @@ public class JsonSchemaGenerator {
     // This hack exists because for Property we generate a oneOf for properties that are not strings.
     // By default, the 'default' is in each oneOf which Monaco editor didn't take into account.
     // So, we pull off the 'default' from any of the oneOf to the parent.
-    private void pullOfDefaultFromOneOf(ObjectNode objectNode) {
+    // same thing for documentation fields: 'title', 'description', '$deprecated'
+    private void pullDocumentationAndDefaultFromOneOf(ObjectNode objectNode) {
         objectNode.findParents("oneOf").forEach(jsonNode -> {
             if (jsonNode instanceof ObjectNode oNode) {
                 JsonNode oneOf = oNode.get("oneOf");
                 if (oneOf instanceof ArrayNode arrayNode) {
                     Iterator<JsonNode> it = arrayNode.elements();
-                    JsonNode defaultNode = null;
-                    while (it.hasNext() && defaultNode == null) {
+                    var nodesToPullUp = new HashMap<String, Optional<JsonNode>>(Map.ofEntries(
+                        Map.entry("default", Optional.empty()),
+                        Map.entry("title", Optional.empty()),
+                        Map.entry("description", Optional.empty()),
+                        Map.entry("$deprecated", Optional.empty())
+                    ));
+                    // find nodes to pull up
+                    while (it.hasNext() && nodesToPullUp.containsValue(Optional.<JsonNode>empty())) {
                         JsonNode next = it.next();
                         if (next instanceof ObjectNode nextAsObj) {
-                            defaultNode = nextAsObj.get("default");
+                            nodesToPullUp.entrySet().stream()
+                                .filter(node -> node.getValue().isEmpty())
+                                .forEach(node -> node
+                                    .setValue(Optional.ofNullable(
+                                        nextAsObj.get(node.getKey())
+                                    )));
                         }
                     }
-                    if (defaultNode != null) {
-                        oNode.set("default", defaultNode);
-                    }
+                    // create nodes on parent
+                    nodesToPullUp.entrySet().stream()
+                        .filter(node -> node.getValue().isPresent())
+                        .forEach(node -> oNode.set(node.getKey(), node.getValue().get()));
                 }
             }
         });
@@ -629,7 +642,7 @@ public class JsonSchemaGenerator {
         try {
             ObjectNode objectNode = generator.generateSchema(cls);
             replaceAnyOfWithOneOf(objectNode);
-            pullOfDefaultFromOneOf(objectNode);
+            pullDocumentationAndDefaultFromOneOf(objectNode);
             removeRequiredOnPropsWithDefaults(objectNode);
 
             return JacksonMapper.toMap(extractMainRef(objectNode));


Test patch:
diff --git a/core/src/test/java/io/kestra/core/docs/JsonSchemaGeneratorTest.java b/core/src/test/java/io/kestra/core/docs/JsonSchemaGeneratorTest.java
index a44686909..cbc0ac7a3 100644
--- a/core/src/test/java/io/kestra/core/docs/JsonSchemaGeneratorTest.java
+++ b/core/src/test/java/io/kestra/core/docs/JsonSchemaGeneratorTest.java
@@ -249,6 +249,30 @@ class JsonSchemaGeneratorTest {
         assertThat((List<String>) generate.get("required"), containsInAnyOrder("requiredWithNoDefault"));
     }
 
+    @SuppressWarnings("unchecked")
+    @Test
+    void testDocumentation() {
+        Map<String, Object> generate = jsonSchemaGenerator.properties(Task.class, TaskWithDynamicDocumentedFields.class);
+        assertThat(generate, is(not(nullValue())));
+        assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("stringProperty").get("title"), is("stringProperty title"));
+        assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("stringProperty").get("description"), is("stringProperty description"));
+        assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("stringProperty").get("$deprecated"), is(true));
+
+        assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("integerProperty").get("title"), is("integerProperty title"));
+        assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("integerProperty").get("description"), is("integerProperty description"));
+        assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("integerProperty").get("$deprecated"), is(true));
+
+        assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("stringPropertyWithDefault").get("title"), is("stringPropertyWithDefault title"));
+        assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("stringPropertyWithDefault").get("description"), is("stringPropertyWithDefault description"));
+        assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("stringPropertyWithDefault").get("$deprecated"), is(true));
+        assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("stringPropertyWithDefault").get("default"), is("my string"));
+
+        assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("integerPropertyWithDefault").get("title"), is("integerPropertyWithDefault title"));
+        assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("integerPropertyWithDefault").get("description"), is("integerPropertyWithDefault description"));
+        assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("integerPropertyWithDefault").get("$deprecated"), is(true));
+        assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("integerPropertyWithDefault").get("default"), is("10000"));
+    }
+
     @SuppressWarnings("unchecked")
     @Test
     void dashboard() throws URISyntaxException {
@@ -314,10 +338,11 @@ class JsonSchemaGeneratorTest {
         private TestEnum testEnum;
 
         @PluginProperty
-        @Schema(title = "Title from the attribute")
+        @Schema(title = "Title from the attribute", description = "Description from the attribute")
         private TestClass testClass;
 
         @PluginProperty(internalStorageURI = true)
+        @Schema(title = "Title from the attribute", description = "Description from the attribute")
         private String uri;
 
         @PluginProperty
@@ -392,4 +417,49 @@ class JsonSchemaGeneratorTest {
         @NotNull
         private Property<TaskWithEnum.TestClass> requiredWithNoDefault;
     }
+
+    @SuperBuilder
+    @ToString
+    @EqualsAndHashCode
+    @Getter
+    @NoArgsConstructor
+    public static class TaskWithDynamicDocumentedFields extends Task implements RunnableTask<VoidOutput>  {
+
+        @Deprecated(since="deprecation_version_1", forRemoval=true)
+        @Schema(
+            title = "integerPropertyWithDefault title",
+            description = "integerPropertyWithDefault description"
+        )
+        @Builder.Default
+        protected Property<Integer> integerPropertyWithDefault = Property.of(10000);
+
+        @Deprecated(since="deprecation_version_1", forRemoval=true)
+        @Schema(
+            title = "stringPropertyWithDefault title",
+            description = "stringPropertyWithDefault description"
+        )
+        @Builder.Default
+        protected Property<String> stringPropertyWithDefault = Property.of("my string");
+
+
+        @Deprecated(since="deprecation_version_1", forRemoval=true)
+        @Schema(
+            title = "stringProperty title",
+            description = "stringProperty description"
+        )
+        protected Property<String> stringProperty;
+
+        @Deprecated(since="deprecation_version_1", forRemoval=true)
+        @Schema(
+            title = "integerProperty title",
+            description = "integerProperty description"
+        )
+        protected Property<Integer> integerProperty;
+
+        @Override
+        public VoidOutput run(RunContext runContext) throws Exception {
+            return null;
+        }
+    }
+
 }