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

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


The following commit(s) were added to refs/heads/v3-0-test by this push:
     new f1efe19ab5f [v3-0-test] Allow non-string valid JSON values in Variable 
import. (#49844) (#50063)
f1efe19ab5f is described below

commit f1efe19ab5fbbcd5cd0ac6a745c9239c2cdfc4ee
Author: Karthikeyan Singaravelan <[email protected]>
AuthorDate: Thu May 1 20:36:17 2025 +0530

    [v3-0-test] Allow non-string valid JSON values in Variable import. (#49844) 
(#50063)
    
    * Allow non-string valid JSON values in Variable import. (#49844)
    
    (cherry picked from commit 61dfd4411c1c37c8eea94be0bf7c0286fa27ffd8)
    
    * fix (airflowctl): Handle custom generated objects (#49917)
    
    ---------
    
    Co-authored-by: Bugra Ozturk <[email protected]>
---
 .../api_fastapi/core_api/datamodels/variables.py   |  4 +--
 .../core_api/openapi/v1-rest-api-generated.yaml    |  4 +--
 .../airflow/ui/openapi-gen/requests/schemas.gen.ts |  5 ++--
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |  4 ++-
 .../ui/src/pages/Variables/ImportVariablesForm.tsx | 18 ++----------
 .../core_api/routes/public/test_variables.py       | 32 ++++++++--------------
 .../src/airflowctl/api/datamodels/generated.py     |  6 +++-
 airflow-ctl/src/airflowctl/ctl/cli_config.py       |  6 +++-
 .../tests/airflow_ctl/api/test_operations.py       | 10 ++++---
 9 files changed, 40 insertions(+), 49 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 2317d8a168b..2905e752650 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 @@ from __future__ import annotations
 
 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 0f8b0f2496d..4ea3c207c3f 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 eb634bb0779..179da514713 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 a2370b2cc77..d34e86cdb55 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 2d414b6cc84..37111f88a6d 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 1509a86b862..e6c359a3eb8 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 @@ class TestPostVariable(TestVariableEndpoint):
             ]
         }
 
-    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 405970db66d..b2f97ae42bf 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/src/airflowctl/ctl/cli_config.py 
b/airflow-ctl/src/airflowctl/ctl/cli_config.py
index b55c93c9922..1ce7b82d8fa 100644
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -346,7 +346,11 @@ class CommandFactory:
                     )
                 )
             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),),
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py 
b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
index d745c777590..74d4863447d 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,

Reply via email to