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

bugraoz93 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new a1cec525543 Fix airflowctl JSON parsing for Dag run conf (#68985)
a1cec525543 is described below

commit a1cec525543c048b492f617e174b6c862664e55e
Author: Atakan Yenel <[email protected]>
AuthorDate: Mon Jul 13 21:44:51 2026 +0100

    Fix airflowctl JSON parsing for Dag run conf (#68985)
    
    * Fix airflowctl JSON parsing for Dag run conf
    
    airflowctl dags trigger --conf rejected JSON object strings because the
    generated parser validated them with raw dict, which cannot consume a JSON
    string. Parse JSON object CLI values into dictionaries before validation so
    conf values such as {"my-key": "my-value"} are accepted.
    
    closes: #67989
    
    * Expand json_dict_type test coverage for airflowctl conf parsing
    
    Cover dict input early return, invalid JSON, and non-object JSON so all
    branches of the conf parser are exercised.
    
    ---------
    
    Co-authored-by: Atakan Yenel <[email protected]>
---
 airflow-ctl/src/airflowctl/ctl/cli_config.py       | 18 ++++++++-
 .../tests/airflow_ctl/ctl/test_cli_config.py       | 44 +++++++++++++++++++++-
 2 files changed, 59 insertions(+), 3 deletions(-)

diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py 
b/airflow-ctl/src/airflowctl/ctl/cli_config.py
index d0901e795ef..40ee7a1c67a 100755
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -24,6 +24,7 @@ import argparse
 import ast
 import datetime
 import inspect
+import json
 import os
 import sys
 from argparse import Namespace
@@ -195,6 +196,19 @@ def string_lower_type(val):
     return val.strip().lower()
 
 
+def json_dict_type(val: str | dict[str, Any]) -> dict[str, Any]:
+    """Parse JSON object argument."""
+    if isinstance(val, dict):
+        return val
+    try:
+        parsed = json.loads(val)
+    except json.JSONDecodeError as e:
+        raise argparse.ArgumentTypeError(f"invalid JSON object: {val!r}") from 
e
+    if not isinstance(parsed, dict):
+        raise argparse.ArgumentTypeError(f"expected JSON object: {val!r}")
+    return parsed
+
+
 def _load_help_texts_yaml() -> dict[str, dict[str, str]]:
     """Load the help texts yaml for the auto-generated commands."""
     help_texts_path = Path(__file__).parent / "help_texts.yaml"
@@ -516,11 +530,11 @@ class CommandFactory:
             "str": str,
             "bytes": bytes,
             "list": list,
-            "dict": dict,
+            "dict": json_dict_type,
             "tuple": tuple,
             "set": set,
             "datetime.datetime": datetime.datetime,
-            "dict[str, typing.Any]": dict,
+            "dict[str, typing.Any]": json_dict_type,
         }
         # Default to ``str`` to preserve previous behaviour for any 
unrecognised
         # type names while still allowing the CLI to function.
diff --git a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py 
b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
index 945b5abb833..281bd2b040b 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
@@ -33,6 +33,7 @@ from airflowctl.ctl.cli_config import (
     CommandFactory,
     GroupCommand,
     add_auth_token_to_all_commands,
+    json_dict_type,
     merge_commands,
     safe_call_command,
 )
@@ -105,7 +106,7 @@ def test_args_create():
                 "help": "dag_run_conf for backfill operation",
                 "action": None,
                 "default": None,
-                "type": dict,
+                "type": json_dict_type,
                 "dest": None,
             },
         ),
@@ -321,6 +322,47 @@ class TestCommandFactory:
         assert is_alive_arg.kwargs["default"] is None
         assert is_alive_arg.kwargs["type"] is bool
 
+    def test_command_factory_parses_json_dict_datamodel_fields(self):
+        """Dict datamodel fields should parse JSON object CLI values."""
+        command_factory = CommandFactory()
+        dags_trigger_args = []
+        for generated_group_command in command_factory.group_commands:
+            if generated_group_command.name != "dags":
+                continue
+            for sub_command in generated_group_command.subcommands:
+                if sub_command.name == "trigger":
+                    dags_trigger_args = list(sub_command.args)
+                    break
+
+        conf_arg = next(arg for arg in dags_trigger_args if arg.flags == 
("--conf",))
+        parsed_conf = conf_arg.kwargs["type"]('{"my-key": "my-value"}')
+
+        assert parsed_conf == {"my-key": "my-value"}
+
+    def test_json_dict_type_returns_dict_input_unchanged(self):
+        """A dict input is returned as-is without re-parsing."""
+        value = {"my-key": "my-value"}
+
+        assert json_dict_type(value) is value
+
+    def test_json_dict_type_parses_json_object(self):
+        """A JSON object string is parsed into a dict."""
+        assert json_dict_type('{"my-key": "my-value"}') == {"my-key": 
"my-value"}
+
+    def test_json_dict_type_rejects_invalid_json(self):
+        """Invalid JSON raises an ArgumentTypeError."""
+        with pytest.raises(argparse.ArgumentTypeError, match="invalid JSON 
object"):
+            json_dict_type("{not valid json}")
+
+    @pytest.mark.parametrize(
+        "value",
+        ["[]", '"string"', "123", "true", "null"],
+    )
+    def test_json_dict_type_rejects_non_object_json(self, value):
+        """Valid JSON that is not an object raises an ArgumentTypeError."""
+        with pytest.raises(argparse.ArgumentTypeError, match="expected JSON 
object"):
+            json_dict_type(value)
+
     def test_command_factory_required_primitive_param_is_positional(self, 
tmp_path):
         """Required primitive parameters (no default, not Optional) become 
positional arguments.
 

Reply via email to