This is an automated email from the ASF dual-hosted git repository.

kaxil pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 7e3b2c64f1d UI: Keep JSON text in string HITL params instead of 
showing [object Object] (#74702)
7e3b2c64f1d is described below

commit 7e3b2c64f1d5169118b5e699c05fb7cca1f60d44
Author: Kaxil Naik <[email protected]>
AuthorDate: Wed Oct 14 13:43:38 2026 +0100

    UI: Keep JSON text in string HITL params instead of showing [object Object] 
(#74702)
    
    
    A HITL param declared as a string whose value looks like a JSON object was 
parsed into an object by the review form. The string field then rendered
    "[object Object]", and the form sent the object back as params_input, so an
    untouched Approve failed the task's param validation and integers above 
2**53
    were rounded. Only parse the value when the declared schema allows an 
object.
    
    
    Values arrive as stored JSON, so an object param's value is already an
    object and the parse only ever turned a string into one. That covered
    untyped params too: Param('{"a": 1}') still came back as a dict.
---
 airflow-core/src/airflow/ui/src/utils/hitl.test.ts | 48 +++++++++++++++++++++-
 airflow-core/src/airflow/ui/src/utils/hitl.ts      | 18 ++------
 2 files changed, 50 insertions(+), 16 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/utils/hitl.test.ts 
b/airflow-core/src/airflow/ui/src/utils/hitl.test.ts
index 60dbb52dc42..309de054b97 100644
--- a/airflow-core/src/airflow/ui/src/utils/hitl.test.ts
+++ b/airflow-core/src/airflow/ui/src/utils/hitl.test.ts
@@ -21,10 +21,12 @@ import { describe, it, expect, vi } from "vitest";
 
 import type { HITLDetail } from "openapi/requests/types.gen";
 
-import { getHITLParamsDict, getHITLState, isHITLPending } from "./hitl";
+import { getHITLFormData, getHITLParamsDict, getHITLState, isHITLPending } 
from "./hitl";
 
 const mockTranslate = vi.fn((key: string) => key) as unknown as TFunction;
 
+const DEFAULT_OUTPUT = '{"title": "Login broken", "priority": 2}';
+
 const createMockHITLDetail = (overrides?: Partial<HITLDetail>): HITLDetail =>
   ({
     assigned_users: [],
@@ -164,4 +166,48 @@ describe("getHITLParamsDict", () => {
 
     expect(paramsDict.freeze_until?.value).toBe("2026-09-03");
   });
+
+  it.each([
+    { expectedType: "string", label: "string", schema: { type: "string" } },
+    { expectedType: ["string", "null"], label: "nullable string", schema: { 
type: ["string", "null"] } },
+    { expectedType: "string", label: "untyped", schema: {} },
+    {
+      expectedType: "string",
+      label: "string holding an integer beyond 2**53",
+      output: '{"id": 12345678901234567890}',
+      schema: { type: "string" },
+    },
+  ])(
+    "keeps a JSON-looking value of a $label param as text",
+    ({ expectedType, output = DEFAULT_OUTPUT, schema }) => {
+      const hitlDetail = createMockHITLDetail({
+        params: {
+          output: { description: null, schema, value: output },
+        },
+      });
+
+      const paramsDict = getHITLParamsDict(hitlDetail, mockTranslate, new 
URLSearchParams());
+
+      expect(paramsDict.output?.value).toBe(output);
+      expect(paramsDict.output?.schema.type).toEqual(expectedType);
+      expect(getHITLFormData(paramsDict).params_input).toEqual({ output });
+    },
+  );
+
+  it.each([
+    { label: "an object", type: "object" },
+    { label: "a nullable object", type: ["object", "null"] },
+  ])("returns the object value of $label param as an object", ({ type }) => {
+    const objectValue = { key: "value", nested: { data: 123 } };
+    const hitlDetail = createMockHITLDetail({
+      params: {
+        objectParam: { description: null, schema: { type }, value: objectValue 
},
+      },
+    });
+
+    const paramsDict = getHITLParamsDict(hitlDetail, mockTranslate, new 
URLSearchParams());
+
+    expect(paramsDict.objectParam?.schema.type).toEqual(type);
+    expect(getHITLFormData(paramsDict).params_input).toEqual({ objectParam: 
objectValue });
+  });
 });
diff --git a/airflow-core/src/airflow/ui/src/utils/hitl.ts 
b/airflow-core/src/airflow/ui/src/utils/hitl.ts
index 64af365f145..ca03b8f03f3 100644
--- a/airflow-core/src/airflow/ui/src/utils/hitl.ts
+++ b/airflow-core/src/airflow/ui/src/utils/hitl.ts
@@ -129,21 +129,9 @@ export const getHITLParamsDict = (
       // default is null" from "bare value".
       const isWrapped = paramData !== undefined && "value" in paramData;
 
-      // Check if there's a preloaded value from URL params
-      let finalValue = hitlDetail.params_input?.[key] ?? (isWrapped ? 
paramData.value : value);
-
-      // If preloaded value is a string that might be JSON, try to parse it
-      if (typeof finalValue === "string" && finalValue.trim().startsWith("{")) 
{
-        try {
-          const parsed: unknown = JSON.parse(finalValue);
-
-          if (typeof parsed === "object" && parsed !== null && 
!Array.isArray(parsed)) {
-            finalValue = parsed;
-          }
-        } catch {
-          // If parsing fails, keep the string value
-        }
-      }
+      // Values arrive as stored JSON, so an object param's value is already 
an object. A string is
+      // never parsed: a JSON-looking string would render as "[object Object]" 
and go back as an object.
+      const finalValue = hitlDetail.params_input?.[key] ?? (isWrapped ? 
paramData.value : value);
 
       const description: string =
         paramData && typeof paramData.description === "string" ? 
paramData.description : "";

Reply via email to