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

amoghdesai 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 e0cd6e246c2 SQL not rendered in Rendered Templates view  (#60739)
e0cd6e246c2 is described below

commit e0cd6e246c288d33f359ec2268b3d342832e9648
Author: Sarthak Vaish <[email protected]>
AuthorDate: Mon Feb 23 15:41:18 2026 +0530

    SQL not rendered in Rendered Templates view  (#60739)
---
 .../src/airflow/sdk/execution_time/task_runner.py  | 34 +++++++++-
 .../task_sdk/execution_time/test_task_runner.py    | 72 ++++++++++++++++++++++
 2 files changed, 103 insertions(+), 3 deletions(-)

diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py 
b/task-sdk/src/airflow/sdk/execution_time/task_runner.py
index 3d641c36fe0..7355f886ee8 100644
--- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py
+++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py
@@ -1005,16 +1005,44 @@ def _serialize_template_field(template_field: Any, 
name: str) -> str | dict | li
 def _serialize_rendered_fields(task: AbstractOperator) -> dict[str, JsonValue]:
     from airflow.sdk._shared.secrets_masker import redact
 
-    rendered_fields = {}
+    rendered_fields: dict[str, JsonValue] = {}
     for field in task.template_fields:
         value = getattr(task, field)
         serialized = _serialize_template_field(value, field)
         # Redact secrets in the task process itself before sending to API 
server
         # This ensures that the secrets those are registered via mask_secret() 
on workers / dag processor are properly masked
         # on the UI.
-        rendered_fields[field] = redact(serialized, field)
+        redacted = redact(serialized, field)
+        rendered_fields[field] = 
TypeAdapter(JsonValue).validate_python(redacted)
+
+    renderers = getattr(task, "template_fields_renderers", {})
+    for renderer_path in renderers:
+        if "." not in renderer_path:
+            continue
+
+        base_field, _, remainder = renderer_path.partition(".")
+        if base_field not in task.template_fields:
+            continue
+
+        base_value = getattr(task, base_field, None)
+        if base_value is None:
+            continue
+
+        current = base_value
+        for key in remainder.split("."):
+            if isinstance(current, dict):
+                current = current.get(key)
+            else:
+                current = getattr(current, key, None)
+            if current is None:
+                break
+
+        if current is not None:
+            serialized = _serialize_template_field(current, renderer_path)
+            redacted = redact(serialized, renderer_path)
+            rendered_fields[renderer_path] = 
TypeAdapter(JsonValue).validate_python(redacted)
 
-    return rendered_fields  # type: ignore[return-value] # Convince mypy that 
this is OK since we pass JsonValue to redact, so it will return the same
+    return rendered_fields
 
 
 def _build_asset_profiles(lineage_objects: list) -> Iterator[AssetProfile]:
diff --git a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py 
b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py
index 3de79732ff5..f40b4399cff 100644
--- a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py
+++ b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py
@@ -2748,6 +2748,78 @@ class TestRuntimeTaskInstance:
             == '[{"name": "var1", "value": "This is a test phrase.", 
"value_from": null}, {"name": "var2", "value": "***", "value_from": null}, 
{"name": "var3", "value": "***", "value_from": null}]'
         )
 
+    def test_nested_template_field_renderer_respects_redaction(
+        self, create_runtime_ti, mock_supervisor_comms
+    ):
+        """
+        Ensure nested template_fields_renderers paths still serialize
+        and redact correctly.
+        """
+
+        class CustomOperator(BaseOperator):
+            template_fields = ("config",)
+            template_fields_renderers = {"config.nested.secret": "json"}
+
+            def __init__(self, config, *args, **kwargs):
+                super().__init__(*args, **kwargs)
+                self.config = config
+
+            def execute(self, context):
+                pass
+
+        config = {"nested": {"secret": "top_secret"}}
+
+        task = CustomOperator(
+            task_id="nested_redact_test",
+            config=config,
+        )
+
+        runtime_ti = create_runtime_ti(task=task)
+
+        with mock.patch(
+            "airflow.sdk._shared.secrets_masker.redact",
+            side_effect=lambda val, _: f"MASKED:{val}",
+        ):
+            run(runtime_ti, context=runtime_ti.get_template_context(), 
log=mock.MagicMock())
+
+        assert any(
+            "config.nested.secret" in call.kwargs.get("msg").rendered_fields
+            for call in mock_supervisor_comms.send.mock_calls
+            if hasattr(call.kwargs.get("msg"), "rendered_fields")
+        )
+
+    def test_rendered_fields_validates_json_value_types(self, 
create_runtime_ti, mock_supervisor_comms):
+        """
+        Ensure validated JSON-compatible types are preserved after redaction.
+        """
+
+        class CustomOperator(BaseOperator):
+            template_fields = ("data",)
+
+            def __init__(self, data, *args, **kwargs):
+                super().__init__(*args, **kwargs)
+                self.data = data
+
+            def execute(self, context):
+                pass
+
+        complex_value = {"key": [1, 2, {"nested": "value"}]}
+
+        task = CustomOperator(task_id="json_validation_test", 
data=complex_value)
+        runtime_ti = create_runtime_ti(task=task)
+
+        with mock.patch(
+            "airflow.sdk._shared.secrets_masker.redact",
+            side_effect=lambda val, _: val,
+        ):
+            run(runtime_ti, context=runtime_ti.get_template_context(), 
log=mock.MagicMock())
+
+        assert any(
+            call.kwargs.get("msg").rendered_fields["data"] == complex_value
+            for call in mock_supervisor_comms.send.mock_calls
+            if hasattr(call.kwargs.get("msg"), "rendered_fields")
+        )
+
 
 class TestXComAfterTaskExecution:
     @pytest.mark.parametrize(

Reply via email to