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
commit 77e8f5582d9083483cadc84733e04808f201ef0b
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Oct 30 01:00:17 2026 +0530
[v3-4-test] Preserve XCom value types when editing in the UI (#75496)
(#75951)
(cherry picked from commit e0a95793c9540cc53a07b5b6f0dfa0ef77d207c6)
Co-authored-by: Anzin Mohammed <[email protected]>
---
.../airflow/ui/src/pages/XCom/XComModal.test.tsx | 60 ++++++++++++++++++++++
.../src/airflow/ui/src/pages/XCom/XComModal.tsx | 6 ++-
2 files changed, 64 insertions(+), 2 deletions(-)
diff --git a/airflow-core/src/airflow/ui/src/pages/XCom/XComModal.test.tsx
b/airflow-core/src/airflow/ui/src/pages/XCom/XComModal.test.tsx
index 9c805a9f4cc..e8feb6d102a 100644
--- a/airflow-core/src/airflow/ui/src/pages/XCom/XComModal.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/XCom/XComModal.test.tsx
@@ -18,10 +18,12 @@
*/
import "@testing-library/jest-dom/vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { http, HttpResponse } from "msw";
import { afterEach, expect, it, vi } from "vitest";
import { XcomService } from "openapi/requests";
+import { server } from "src/mocks/server";
import { Wrapper } from "src/utils/Wrapper";
import XComModal from "./XComModal";
@@ -80,3 +82,61 @@ it("edits the selected regional XCom without using the
colliding public map coor
);
expect(fetch).toHaveBeenCalledWith(expect.objectContaining({ regionId,
regionIndex: 3 }));
});
+
+it.each([
+ { label: "numeric string", value: "123" },
+ { label: "boolean string", value: "false" },
+ { label: "null string", value: "null" },
+ { label: "object string", value: '{"count":1}' },
+ { label: "array string", value: "[1,2]" },
+ { label: "quoted string", value: '"quoted"' },
+ { label: "empty string", value: "" },
+ { label: "multiline string", value: "first\nsecond" },
+])("preserves a stored $label when saving without edits", async ({ value }) =>
{
+ let deserialize: string | null = null;
+ let stringify: string | null = null;
+ const update = vi.fn();
+
+ server.use(
+
http.get("/api/v2/dags/dag/dagRuns/run/taskInstances/task/xcomEntries/return_value",
({ request }) => {
+ const url = new URL(request.url);
+
+ deserialize = url.searchParams.get("deserialize");
+ stringify = url.searchParams.get("stringify");
+
+ return HttpResponse.json({ key: "return_value", value });
+ }),
+ http.patch(
+
"/api/v2/dags/dag/dagRuns/run/taskInstances/task/xcomEntries/return_value",
+ async ({ request }) => {
+ update(await request.json());
+
+ return HttpResponse.json({ key: "return_value", value });
+ },
+ ),
+ );
+
+ render(
+ <XComModal
+ dagId="dag"
+ isOpen
+ mapIndex={-1}
+ mode="edit"
+ onClose={vi.fn()}
+ runId="run"
+ taskId="task"
+ xcomKey="return_value"
+ />,
+ { wrapper: Wrapper },
+ );
+
+ const editorValue = JSON.stringify(value, undefined, 2);
+
+ await waitFor(() =>
expect(screen.getByTestId("editor").textContent).toBe(editorValue));
+ expect(deserialize).toBe("false");
+ expect(stringify).toBe("false");
+
+ fireEvent.click(screen.getByRole("button", { name: "common:modal.save" }));
+
+ await waitFor(() => expect(update).toHaveBeenCalledWith({ map_index: -1,
value }));
+});
diff --git a/airflow-core/src/airflow/ui/src/pages/XCom/XComModal.tsx
b/airflow-core/src/airflow/ui/src/pages/XCom/XComModal.tsx
index 7ab05e32e79..90edfcac8b0 100644
--- a/airflow-core/src/airflow/ui/src/pages/XCom/XComModal.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/XCom/XComModal.tsx
@@ -72,10 +72,12 @@ const XComModal = ({
{
dagId,
dagRunId: runId,
- deserialize: true,
+ // Editing needs the stored type; display deserialization can parse
JSON-looking strings.
+ deserialize: false,
mapIndex,
regionId,
regionIndex,
+ stringify: false,
taskId,
xcomKey: xcomKey ?? "",
},
@@ -88,7 +90,7 @@ const XComModal = ({
if (isOpen && isEditMode && data) {
const val = data.value;
- setValue(typeof val === "string" ? val : JSON.stringify(val, undefined,
2));
+ setValue(JSON.stringify(val, undefined, 2));
}
}, [data, isEditMode, isOpen]);