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

vatsrahul1001 pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


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

commit 4fcb7ae1d790d27465c064131e192e5cab1b9ce7
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sun Oct 18 16:08:36 2026 +0530

    [v3-4-test] UI: Keep JSON text in string HITL params instead of showing 
[object Object] (#74702) (#74961)
    
    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.
    (cherry picked from commit 7e3b2c64f1d5169118b5e699c05fb7cca1f60d44)
    
    Co-authored-by: Kaxil Naik <[email protected]>
---
 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 573dab8c9d8..c7cf2330998 100644
--- a/airflow-core/src/airflow/ui/src/utils/hitl.ts
+++ b/airflow-core/src/airflow/ui/src/utils/hitl.ts
@@ -127,21 +127,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