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,