From 36b36e421133f0b9eb5dcbb5d95cc4f6a6b6b890 Mon Sep 17 00:00:00 2001
From: Karthikeyan Singaravelan <tir.karthi@gmail.com>
Date: Wed, 30 Apr 2025 21:22:09 +0530
Subject: [PATCH 1/2] Allow non-string valid JSON values in Variable import.
 (#49844)

(cherry picked from commit 61dfd4411c1c37c8eea94be0bf7c0286fa27ffd8)
---
 .../core_api/datamodels/variables.py          |  4 +--
 .../openapi/v1-rest-api-generated.yaml        |  4 +--
 .../ui/openapi-gen/requests/schemas.gen.ts    |  5 +--
 .../ui/openapi-gen/requests/types.gen.ts      |  4 ++-
 .../pages/Variables/ImportVariablesForm.tsx   | 18 ++---------
 .../core_api/routes/public/test_variables.py  | 32 +++++++------------
 .../airflowctl/api/datamodels/generated.py    |  6 +++-
 .../tests/airflow_ctl/api/test_operations.py  | 10 +++---
 8 files changed, 35 insertions(+), 48 deletions(-)

diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/variables.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/variables.py
index 2317d8a168b82..2905e752650cd 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/variables.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/variables.py
@@ -19,7 +19,7 @@
 
 import json
 
-from pydantic import Field, model_validator
+from pydantic import Field, JsonValue, model_validator
 
 from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel
 from airflow.models.base import ID_LEN
@@ -54,7 +54,7 @@ class VariableBody(StrictBaseModel):
     """Variable serializer for bodies."""
 
     key: str = Field(max_length=ID_LEN)
-    value: str = Field(serialization_alias="val")
+    value: JsonValue = Field(serialization_alias="val")
     description: str | None = Field(default=None)
 
 
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-rest-api-generated.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-rest-api-generated.yaml
index 0f8b0f2496d58..4ea3c207c3f06 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-rest-api-generated.yaml
+++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-rest-api-generated.yaml
@@ -9009,6 +9009,7 @@ components:
       - unixname
       title: JobResponse
       description: Job serializer for responses.
+    JsonValue: {}
     PatchTaskInstanceBody:
       properties:
         new_state:
@@ -10249,8 +10250,7 @@ components:
           maxLength: 250
           title: Key
         value:
-          type: string
-          title: Value
+          $ref: '#/components/schemas/JsonValue'
         description:
           anyOf:
           - type: string
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index eb634bb077999..179da51471303 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -3392,6 +3392,8 @@ export const $JobResponse = {
   description: "Job serializer for responses.",
 } as const;
 
+export const $JsonValue = {} as const;
+
 export const $PatchTaskInstanceBody = {
   properties: {
     new_state: {
@@ -5386,8 +5388,7 @@ export const $VariableBody = {
       title: "Key",
     },
     value: {
-      type: "string",
-      title: "Value",
+      $ref: "#/components/schemas/JsonValue",
     },
     description: {
       anyOf: [
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index a2370b2cc7759..d34e86cdb55bb 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -890,6 +890,8 @@ export type JobResponse = {
   unixname: string | null;
 };
 
+export type JsonValue = unknown;
+
 /**
  * Request body for Clear Task Instances endpoint.
  */
@@ -1297,7 +1299,7 @@ export type ValidationError = {
  */
 export type VariableBody = {
   key: string;
-  value: string;
+  value: JsonValue;
   description?: string | null;
 };
 
diff --git a/airflow-core/src/airflow/ui/src/pages/Variables/ImportVariablesForm.tsx b/airflow-core/src/airflow/ui/src/pages/Variables/ImportVariablesForm.tsx
index 2d414b6cc843b..37111f88a6d06 100644
--- a/airflow-core/src/airflow/ui/src/pages/Variables/ImportVariablesForm.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Variables/ImportVariablesForm.tsx
@@ -58,7 +58,7 @@ const ImportVariablesForm = ({ onClose }: ImportVariablesFormProps) => {
 
   const [actionIfExists, setActionIfExists] = useState<"fail" | "overwrite" | "skip">("fail");
   const [isParsing, setIsParsing] = useState(false);
-  const [fileContent, setFileContent] = useState<Record<string, string> | undefined>(undefined);
+  const [fileContent, setFileContent] = useState<Record<string, unknown> | undefined>(undefined);
 
   const onFileChange = (file: File) => {
     setIsParsing(true);
@@ -67,21 +67,9 @@ const ImportVariablesForm = ({ onClose }: ImportVariablesFormProps) => {
     reader.addEventListener("load", (event) => {
       try {
         const text = event.target?.result as string;
-        const parsedContent = JSON.parse(text) as unknown;
+        const parsedContent = JSON.parse(text) as Record<string, unknown>;
 
-        if (
-          typeof parsedContent === "object" &&
-          parsedContent !== null &&
-          Object.entries(parsedContent).every(
-            ([key, value]) => typeof key === "string" && typeof value === "string",
-          )
-        ) {
-          const typedContent = parsedContent as Record<string, string>;
-
-          setFileContent(typedContent);
-        } else {
-          throw new Error("Invalid JSON format");
-        }
+        setFileContent(parsedContent);
       } catch {
         setError({
           body: {
diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_variables.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_variables.py
index 1509a86b8624d..e6c359a3eb8e0 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_variables.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_variables.py
@@ -478,25 +478,6 @@ def test_post_should_respond_422_when_key_too_large(self, test_client):
             ]
         }
 
-    def test_post_should_respond_422_when_value_is_null(self, test_client):
-        body = {
-            "key": "null value key",
-            "value": None,
-            "description": "key too large",
-        }
-        response = test_client.post("/variables", json=body)
-        assert response.status_code == 422
-        assert response.json() == {
-            "detail": [
-                {
-                    "type": "string_type",
-                    "loc": ["body", "value"],
-                    "msg": "Input should be a valid string",
-                    "input": None,
-                }
-            ]
-        }
-
     @pytest.mark.parametrize(
         "body",
         [
@@ -728,7 +709,13 @@ class TestBulkVariables(TestVariableEndpoint):
                     "actions": [
                         {
                             "action": "create",
-                            "entities": [{"key": "new_var1", "value": "new_value1"}],
+                            "entities": [
+                                {"key": "new_var1", "value": "new_value1"},
+                                {"key": "new_var2", "value": ["new_value1"]},
+                                {"key": "new_var3", "value": 1},
+                                {"key": "new_var4", "value": None},
+                                {"key": "new_var5", "value": {"foo": "bar"}},
+                            ],
                             "action_on_existence": "skip",
                         },
                         {
@@ -750,7 +737,10 @@ class TestBulkVariables(TestVariableEndpoint):
                     ]
                 },
                 {
-                    "create": {"success": ["new_var1"], "errors": []},
+                    "create": {
+                        "success": ["new_var1", "new_var2", "new_var3", "new_var4", "new_var5"],
+                        "errors": [],
+                    },
                     "update": {"success": ["test_variable_key"], "errors": []},
                     "delete": {"success": ["dictionary_password"], "errors": []},
                 },
diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
index 405970db66d7c..b2f97ae42bf05 100644
--- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
+++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
@@ -571,6 +571,10 @@ class JobResponse(BaseModel):
     unixname: Annotated[str | None, Field(title="Unixname")] = None
 
 
+class JsonValue(RootModel[Any]):
+    root: Any
+
+
 class PluginResponse(BaseModel):
     """
     Plugin serializer.
@@ -837,7 +841,7 @@ class VariableBody(BaseModel):
         extra="forbid",
     )
     key: Annotated[str, Field(max_length=250, title="Key")]
-    value: Annotated[str, Field(title="Value")]
+    value: JsonValue
     description: Annotated[str | None, Field(title="Description")] = None
 
 
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
index d745c777590e9..74d4863447d27 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
@@ -661,10 +661,12 @@ class TestVariablesOperations:
     key = "key"
     value = "val"
     description = "description"
-    variable = VariableBody(
-        key=key,
-        value=value,
-        description=description,
+    variable = VariableBody.model_validate(
+        {
+            "key": key,
+            "value": value,
+            "description": description,
+        }
     )
     variable_response = VariableResponse(
         key=key,

From 24952c28b046cdca01cd3cced3e9f98c92a8b93b Mon Sep 17 00:00:00 2001
From: Bugra Ozturk <bugraoz93@users.noreply.github.com>
Date: Tue, 29 Apr 2025 01:06:03 +0200
Subject: [PATCH 2/2] fix (airflowctl): Handle custom generated objects
 (#49917)

---
 airflow-ctl/src/airflowctl/ctl/cli_config.py | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py b/airflow-ctl/src/airflowctl/ctl/cli_config.py
index b55c93c992287..1ce7b82d8fa6b 100644
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -346,7 +346,11 @@ def _create_arg_for_non_primitive_type(
                     )
                 )
             else:
-                annotation = field_type.annotation.__args__[0]
+                try:
+                    annotation = field_type.annotation.__args__[0]
+                except AttributeError:
+                    annotation = field_type.annotation
+
                 commands.append(
                     self._create_arg(
                         arg_flags=("--" + self._sanitize_arg_parameter_key(field),),
