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

mobuchowski 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 6f358052a42 Send task inlets and outlets as JSON lists in OpenLineage 
facet (#74666)
6f358052a42 is described below

commit 6f358052a423abfd9eacc128680089cef1885c0d
Author: Frank Ye <[email protected]>
AuthorDate: Mon Oct 19 05:23:24 2026 -0600

    Send task inlets and outlets as JSON lists in OpenLineage facet (#74666)
    
    * Send task inlets and outlets as JSON lists in OpenLineage facet
    
    * Add list keys to OpenLineage system test events and keep outlets with bad 
keys
    
    * Make OpenLineage asset list tests pass on Airflow 2 and 3.0
---
 .../airflow/providers/openlineage/utils/utils.py   |  19 ++++
 .../openlineage_base_complex_dag__af2.json         |  56 +++++++++++
 .../openlineage_base_complex_dag__af3.json         |  62 +++++++++++++
 .../openlineage_base_simple_dag__af2.json          |   4 +
 .../openlineage_base_simple_dag__af3.json          |   4 +
 .../openlineage_custom_operator_failure_dag.json   |   2 +
 .../openlineage_manual_lineage_dag.json            |   4 +
 .../openlineage_taskflow_simple_dag.json           |   4 +
 .../tests/unit/openlineage/utils/test_utils.py     | 102 ++++++++++++++++++++-
 9 files changed, 256 insertions(+), 1 deletion(-)

diff --git 
a/providers/openlineage/src/airflow/providers/openlineage/utils/utils.py 
b/providers/openlineage/src/airflow/providers/openlineage/utils/utils.py
index 6c53cc6dcfe..ca43441870d 100644
--- a/providers/openlineage/src/airflow/providers/openlineage/utils/utils.py
+++ b/providers/openlineage/src/airflow/providers/openlineage/utils/utils.py
@@ -1298,6 +1298,12 @@ class TaskInfo(InfoJsonEncodable):
     def __init__(self, obj, runtime_assets: list[tuple[Asset, AssetAlias | 
None]] | None = None):
         self._runtime_assets = runtime_assets  # Will not be included in final 
dict, not in self._fields.
         super().__init__(obj)
+        # ``inlets`` / ``outlets`` are a Python repr string that consumers 
cannot reliably parse
+        # (``extra`` can hold quotes, ``Decimal``, ``True``, ...). Expose them 
as real lists too.
+        # Set after ``super().__init__`` so ``_cast_basic_types`` does not 
turn them into strings.
+        for field in ("inlets", "outlets"):
+            if hasattr(self, field):
+                self[f"{field}_list"] = 
_convert_to_json_safe_list(getattr(self, field), field)
 
     def _extend_fields(self) -> None:
         if not self._runtime_assets:
@@ -2203,6 +2209,19 @@ class OpenLineageRedactor(SecretsMasker):
         return item
 
 
+def _convert_to_json_safe_list(value: list, name: str) -> list | None:
+    """
+    Return ``value`` with only JSON-native types.
+
+    Non-serializable values are cast with ``str()``; dict keys JSON can't 
encode (e.g. tuples) are dropped.
+    """
+    try:
+        return json.loads(json.dumps(value, default=str, skipkeys=True))
+    except (TypeError, ValueError) as e:
+        log.warning("Unable to serialize task `%s` to a JSON list: %s", name, 
e)
+        return None
+
+
 def is_json_serializable(item):
     try:
         json.dumps(item)
diff --git 
a/providers/openlineage/tests/system/openlineage/expected_events/openlineage_base_complex_dag__af2.json
 
b/providers/openlineage/tests/system/openlineage/expected_events/openlineage_base_complex_dag__af2.json
index 413af8d8935..4e22417b9a4 100644
--- 
a/providers/openlineage/tests/system/openlineage/expected_events/openlineage_base_complex_dag__af2.json
+++ 
b/providers/openlineage/tests/system/openlineage/expected_events/openlineage_base_complex_dag__af2.json
@@ -331,6 +331,7 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": "{{ result == true }}",
                         "inlets": "[]",
+                        "inlets_list": [],
                         "is_setup": false,
                         "is_teardown": false,
                         "mapped": false,
@@ -339,6 +340,7 @@
                         "operator_class_path": 
"airflow.providers.standard.operators.bash.BashOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "owner'2",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -509,6 +511,7 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": "{{ result == true }}",
                         "inlets": "[]",
+                        "inlets_list": [],
                         "is_setup": false,
                         "is_teardown": false,
                         "mapped": false,
@@ -517,6 +520,7 @@
                         "operator_class_path": 
"airflow.providers.standard.operators.bash.BashOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "owner'2",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -685,6 +689,16 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": "{{ result == true }}",
                         "inlets": "[{'uri': 'file://host1/dir2/file2.txt', 
'extra': None, 'type': 'asset'}, {'uri': 
'unknown_scheme://host1/dir2/file3.txt', 'extra': None, 'type': 'asset'}]",
+                        "inlets_list": [
+                            {
+                                "uri": "file://host1/dir2/file2.txt",
+                                "type": "asset"
+                            },
+                            {
+                                "uri": "unknown_scheme://host1/dir2/file3.txt",
+                                "type": "asset"
+                            }
+                        ],
                         "is_setup": false,
                         "is_teardown": false,
                         "mapped": false,
@@ -694,6 +708,7 @@
                         "operator_class_path": 
"airflow.providers.standard.operators.python.PythonOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -867,6 +882,16 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": "{{ result == true }}",
                         "inlets": "[{'uri': 'file://host1/dir2/file2.txt', 
'extra': None, 'type': 'asset'}, {'uri': 
'unknown_scheme://host1/dir2/file3.txt', 'extra': None, 'type': 'asset'}]",
+                        "inlets_list": [
+                            {
+                                "uri": "file://host1/dir2/file2.txt",
+                                "type": "asset"
+                            },
+                            {
+                                "uri": "unknown_scheme://host1/dir2/file3.txt",
+                                "type": "asset"
+                            }
+                        ],
                         "is_setup": false,
                         "is_teardown": false,
                         "mapped": false,
@@ -876,6 +901,7 @@
                         "operator_class_path": 
"airflow.providers.standard.operators.python.PythonOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -1042,6 +1068,7 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": "{{ result == true }}",
                         "inlets": "[]",
+                        "inlets_list": [],
                         "is_setup": false,
                         "is_teardown": false,
                         "mapped": false,
@@ -1050,6 +1077,12 @@
                         "operator_class_path": 
"airflow.providers.standard.operators.empty.EmptyOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[{'uri': 'file://host/dir/file.txt', 
'extra': None, 'type': 'asset'}]",
+                        "outlets_list": [
+                            {
+                                "uri": "file://host/dir/file.txt",
+                                "type": "asset"
+                            }
+                        ],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -1217,6 +1250,7 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": "{{ result == true }}",
                         "inlets": "[]",
+                        "inlets_list": [],
                         "is_setup": false,
                         "is_teardown": false,
                         "mapped": false,
@@ -1225,6 +1259,12 @@
                         "operator_class_path": 
"airflow.providers.standard.operators.empty.EmptyOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[{'uri': 'file://host/dir/file.txt', 
'extra': None, 'type': 'asset'}]",
+                        "outlets_list": [
+                            {
+                                "uri": "file://host/dir/file.txt",
+                                "type": "asset"
+                            }
+                        ],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -1388,6 +1428,7 @@
                         "external_task_id": "external_task_id",
                         "ignore_first_depends_on_past": "{{ result == true }}",
                         "inlets": "[]",
+                        "inlets_list": [],
                         "is_setup": false,
                         "is_teardown": false,
                         "mapped": false,
@@ -1397,6 +1438,7 @@
                         "operator_class": "SomeCustomOperator",
                         "operator_class_path": "{{ 
result.endswith('openlineage_base_complex_dag.SomeCustomOperator') }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "owner3",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -1552,6 +1594,7 @@
                         "external_task_id": "external_task_id",
                         "ignore_first_depends_on_past": "{{ result == true }}",
                         "inlets": "[]",
+                        "inlets_list": [],
                         "is_setup": false,
                         "is_teardown": false,
                         "mapped": false,
@@ -1561,6 +1604,7 @@
                         "operator_class": "SomeCustomOperator",
                         "operator_class_path": "{{ 
result.endswith('openlineage_base_complex_dag.SomeCustomOperator') }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "owner3",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -1712,6 +1756,7 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": "{{ result == true }}",
                         "inlets": "[]",
+                        "inlets_list": [],
                         "is_setup": false,
                         "is_teardown": false,
                         "mapped": "{{ result == true }}",
@@ -1719,6 +1764,7 @@
                         "operator_class": "CustomMappedOperator",
                         "operator_class_path": "{{ 
result.endswith('openlineage_base_complex_dag.CustomMappedOperator') }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -1880,6 +1926,7 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": "{{ result == true }}",
                         "inlets": "[]",
+                        "inlets_list": [],
                         "is_setup": false,
                         "is_teardown": false,
                         "mapped": "{{ result == true }}",
@@ -1887,6 +1934,7 @@
                         "operator_class": "CustomMappedOperator",
                         "operator_class_path": "{{ 
result.endswith('openlineage_base_complex_dag.CustomMappedOperator') }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -2047,6 +2095,7 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": "{{ result == true }}",
                         "inlets": "[]",
+                        "inlets_list": [],
                         "is_setup": false,
                         "is_teardown": false,
                         "mapped": false,
@@ -2055,6 +2104,7 @@
                         "operator_class_path": 
"airflow.providers.standard.operators.empty.EmptyOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "sla": "123.0 seconds",
@@ -2226,6 +2276,7 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": "{{ result == true }}",
                         "inlets": "[]",
+                        "inlets_list": [],
                         "is_setup": false,
                         "is_teardown": false,
                         "mapped": false,
@@ -2234,6 +2285,7 @@
                         "operator_class_path": 
"airflow.providers.standard.operators.empty.EmptyOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "sla": "123.0 seconds",
@@ -2404,6 +2456,7 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": "{{ result == true }}",
                         "inlets": "[]",
+                        "inlets_list": [],
                         "is_setup": false,
                         "is_teardown": false,
                         "mapped": false,
@@ -2412,6 +2465,7 @@
                         "operator_class_path": 
"airflow.providers.standard.operators.python.PythonOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -2581,6 +2635,7 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": "{{ result == true }}",
                         "inlets": "[]",
+                        "inlets_list": [],
                         "is_setup": false,
                         "is_teardown": false,
                         "mapped": false,
@@ -2589,6 +2644,7 @@
                         "operator_class_path": 
"airflow.providers.standard.operators.python.PythonOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
diff --git 
a/providers/openlineage/tests/system/openlineage/expected_events/openlineage_base_complex_dag__af3.json
 
b/providers/openlineage/tests/system/openlineage/expected_events/openlineage_base_complex_dag__af3.json
index 1c0d509d3ba..e7527369849 100644
--- 
a/providers/openlineage/tests/system/openlineage/expected_events/openlineage_base_complex_dag__af3.json
+++ 
b/providers/openlineage/tests/system/openlineage/expected_events/openlineage_base_complex_dag__af3.json
@@ -332,12 +332,14 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": false,
                         "inlets": "[]",
+                        "inlets_list": [],
                         "mapped": false,
                         "multiple_outputs": false,
                         "operator_class": "BashOperator",
                         "operator_class_path": 
"airflow.providers.standard.operators.bash.BashOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "owner'2",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -505,12 +507,14 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": false,
                         "inlets": "[]",
+                        "inlets_list": [],
                         "mapped": false,
                         "multiple_outputs": false,
                         "operator_class": "BashOperator",
                         "operator_class_path": 
"airflow.providers.standard.operators.bash.BashOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "owner'2",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -677,6 +681,18 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": false,
                         "inlets": "[{'uri': 'file://host1/dir2/file2.txt', 
'extra': {}, 'type': 'asset'}, {'uri': 'unknown_scheme://host1/dir2/file3.txt', 
'extra': {}, 'type': 'asset'}]",
+                        "inlets_list": [
+                            {
+                                "uri": "file://host1/dir2/file2.txt",
+                                "extra": {},
+                                "type": "asset"
+                            },
+                            {
+                                "uri": "unknown_scheme://host1/dir2/file3.txt",
+                                "extra": {},
+                                "type": "asset"
+                            }
+                        ],
                         "mapped": false,
                         "max_retry_delay": "42.0 seconds",
                         "multiple_outputs": false,
@@ -684,6 +700,7 @@
                         "operator_class_path": 
"airflow.providers.standard.operators.python.PythonOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -854,6 +871,18 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": false,
                         "inlets": "[{'uri': 'file://host1/dir2/file2.txt', 
'extra': {}, 'type': 'asset'}, {'uri': 'unknown_scheme://host1/dir2/file3.txt', 
'extra': {}, 'type': 'asset'}]",
+                        "inlets_list": [
+                            {
+                                "uri": "file://host1/dir2/file2.txt",
+                                "extra": {},
+                                "type": "asset"
+                            },
+                            {
+                                "uri": "unknown_scheme://host1/dir2/file3.txt",
+                                "extra": {},
+                                "type": "asset"
+                            }
+                        ],
                         "mapped": false,
                         "max_retry_delay": "42.0 seconds",
                         "multiple_outputs": false,
@@ -861,6 +890,7 @@
                         "operator_class_path": 
"airflow.providers.standard.operators.python.PythonOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -1025,12 +1055,20 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": false,
                         "inlets": "[]",
+                        "inlets_list": [],
                         "mapped": false,
                         "multiple_outputs": false,
                         "operator_class": "EmptyOperator",
                         "operator_class_path": 
"airflow.providers.standard.operators.empty.EmptyOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[{'uri': 'file://host/dir/file.txt', 
'extra': {}, 'type': 'asset'}]",
+                        "outlets_list": [
+                            {
+                                "uri": "file://host/dir/file.txt",
+                                "extra": {},
+                                "type": "asset"
+                            }
+                        ],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -1195,12 +1233,20 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": false,
                         "inlets": "[]",
+                        "inlets_list": [],
                         "mapped": false,
                         "multiple_outputs": false,
                         "operator_class": "EmptyOperator",
                         "operator_class_path": 
"airflow.providers.standard.operators.empty.EmptyOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[{'uri': 'file://host/dir/file.txt', 
'extra': {}, 'type': 'asset'}]",
+                        "outlets_list": [
+                            {
+                                "uri": "file://host/dir/file.txt",
+                                "extra": {},
+                                "type": "asset"
+                            }
+                        ],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -1362,6 +1408,7 @@
                         "external_task_id": "external_task_id",
                         "ignore_first_depends_on_past": false,
                         "inlets": "[]",
+                        "inlets_list": [],
                         "mapped": false,
                         "max_active_tis_per_dag": 7,
                         "max_active_tis_per_dagrun": 2,
@@ -1369,6 +1416,7 @@
                         "operator_class": "SomeCustomOperator",
                         "operator_class_path": "{{ 
result.endswith('openlineage_base_complex_dag.SomeCustomOperator') }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "owner3",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -1521,6 +1569,7 @@
                         "external_task_id": "external_task_id",
                         "ignore_first_depends_on_past": false,
                         "inlets": "[]",
+                        "inlets_list": [],
                         "mapped": false,
                         "max_active_tis_per_dag": 7,
                         "max_active_tis_per_dagrun": 2,
@@ -1528,6 +1577,7 @@
                         "operator_class": "SomeCustomOperator",
                         "operator_class_path": "{{ 
result.endswith('openlineage_base_complex_dag.SomeCustomOperator') }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "owner3",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -1677,11 +1727,13 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": false,
                         "inlets": "[]",
+                        "inlets_list": [],
                         "mapped": "{{ result == true }}",
                         "multiple_outputs": false,
                         "operator_class": "CustomMappedOperator",
                         "operator_class_path": "{{ 
result.endswith('openlineage_base_complex_dag.CustomMappedOperator') }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -1840,11 +1892,13 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": false,
                         "inlets": "[]",
+                        "inlets_list": [],
                         "mapped": "{{ result == true }}",
                         "multiple_outputs": false,
                         "operator_class": "CustomMappedOperator",
                         "operator_class_path": "{{ 
result.endswith('openlineage_base_complex_dag.CustomMappedOperator') }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -2003,12 +2057,14 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": false,
                         "inlets": "[]",
+                        "inlets_list": [],
                         "mapped": false,
                         "multiple_outputs": false,
                         "operator_class": "EmptyOperator",
                         "operator_class_path": 
"airflow.providers.standard.operators.empty.EmptyOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -2176,12 +2232,14 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": false,
                         "inlets": "[]",
+                        "inlets_list": [],
                         "mapped": false,
                         "multiple_outputs": false,
                         "operator_class": "EmptyOperator",
                         "operator_class_path": 
"airflow.providers.standard.operators.empty.EmptyOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -2349,12 +2407,14 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": false,
                         "inlets": "[]",
+                        "inlets_list": [],
                         "mapped": false,
                         "multiple_outputs": false,
                         "operator_class": "PythonOperator",
                         "operator_class_path": 
"airflow.providers.standard.operators.python.PythonOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -2521,12 +2581,14 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": false,
                         "inlets": "[]",
+                        "inlets_list": [],
                         "mapped": false,
                         "multiple_outputs": false,
                         "operator_class": "PythonOperator",
                         "operator_class_path": 
"airflow.providers.standard.operators.python.PythonOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
diff --git 
a/providers/openlineage/tests/system/openlineage/expected_events/openlineage_base_simple_dag__af2.json
 
b/providers/openlineage/tests/system/openlineage/expected_events/openlineage_base_simple_dag__af2.json
index b3ba5e56bcf..884c785dc85 100644
--- 
a/providers/openlineage/tests/system/openlineage/expected_events/openlineage_base_simple_dag__af2.json
+++ 
b/providers/openlineage/tests/system/openlineage/expected_events/openlineage_base_simple_dag__af2.json
@@ -154,6 +154,7 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": true,
                         "inlets": "[]",
+                        "inlets_list": [],
                         "is_setup": false,
                         "is_teardown": false,
                         "mapped": false,
@@ -162,6 +163,7 @@
                         "operator_class_path": 
"airflow.providers.standard.operators.python.PythonOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -285,6 +287,7 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": true,
                         "inlets": "[]",
+                        "inlets_list": [],
                         "is_setup": false,
                         "is_teardown": false,
                         "mapped": false,
@@ -293,6 +296,7 @@
                         "operator_class_path": 
"airflow.providers.standard.operators.python.PythonOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
diff --git 
a/providers/openlineage/tests/system/openlineage/expected_events/openlineage_base_simple_dag__af3.json
 
b/providers/openlineage/tests/system/openlineage/expected_events/openlineage_base_simple_dag__af3.json
index 0652f3d3de8..672e8dfab10 100644
--- 
a/providers/openlineage/tests/system/openlineage/expected_events/openlineage_base_simple_dag__af3.json
+++ 
b/providers/openlineage/tests/system/openlineage/expected_events/openlineage_base_simple_dag__af3.json
@@ -153,12 +153,14 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": false,
                         "inlets": "[]",
+                        "inlets_list": [],
                         "mapped": false,
                         "multiple_outputs": false,
                         "operator_class": "PythonOperator",
                         "operator_class_path": 
"airflow.providers.standard.operators.python.PythonOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -279,12 +281,14 @@
                         "executor_config": {},
                         "ignore_first_depends_on_past": false,
                         "inlets": "[]",
+                        "inlets_list": [],
                         "mapped": false,
                         "multiple_outputs": false,
                         "operator_class": "PythonOperator",
                         "operator_class_path": 
"airflow.providers.standard.operators.python.PythonOperator",
                         "operator_provider_version": "{{ regex_match(result, 
\"^[\\d]+\\.[\\d]+\\.[\\d]+.*\") }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
diff --git 
a/providers/openlineage/tests/system/openlineage/expected_events/openlineage_custom_operator_failure_dag.json
 
b/providers/openlineage/tests/system/openlineage/expected_events/openlineage_custom_operator_failure_dag.json
index b66a4acae33..da4b7f60472 100644
--- 
a/providers/openlineage/tests/system/openlineage/expected_events/openlineage_custom_operator_failure_dag.json
+++ 
b/providers/openlineage/tests/system/openlineage/expected_events/openlineage_custom_operator_failure_dag.json
@@ -87,11 +87,13 @@
                         "downstream_task_ids": "['empty_success']",
                         "executor_config": {},
                         "inlets": "[]",
+                        "inlets_list": [],
                         "mapped": false,
                         "multiple_outputs": false,
                         "operator_class": "FailingOLOperator",
                         "operator_class_path": "{{ 
result.endswith('.FailingOLOperator') }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "some_owner1",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
diff --git 
a/providers/openlineage/tests/system/openlineage/expected_events/openlineage_manual_lineage_dag.json
 
b/providers/openlineage/tests/system/openlineage/expected_events/openlineage_manual_lineage_dag.json
index 0a0af4aabb8..452f3551746 100644
--- 
a/providers/openlineage/tests/system/openlineage/expected_events/openlineage_manual_lineage_dag.json
+++ 
b/providers/openlineage/tests/system/openlineage/expected_events/openlineage_manual_lineage_dag.json
@@ -63,10 +63,12 @@
                         "downstream_task_ids": "['datasets_maximal']",
                         "executor_config": {},
                         "inlets": "[]",
+                        "inlets_list": [],
                         "multiple_outputs": false,
                         "operator_class": "{{ result is string }}",
                         "operator_class_path": "{{ result is string }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -236,10 +238,12 @@
                         "downstream_task_ids": "['query_minimal']",
                         "executor_config": {},
                         "inlets": "[]",
+                        "inlets_list": [],
                         "multiple_outputs": false,
                         "operator_class": "{{ result is string }}",
                         "operator_class_path": "{{ result is string }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
diff --git 
a/providers/openlineage/tests/system/openlineage/expected_events/openlineage_taskflow_simple_dag.json
 
b/providers/openlineage/tests/system/openlineage/expected_events/openlineage_taskflow_simple_dag.json
index 2c7ebed8ec1..88616cab2f6 100644
--- 
a/providers/openlineage/tests/system/openlineage/expected_events/openlineage_taskflow_simple_dag.json
+++ 
b/providers/openlineage/tests/system/openlineage/expected_events/openlineage_taskflow_simple_dag.json
@@ -148,11 +148,13 @@
                         "downstream_task_ids": "['check_events']",
                         "executor_config": {},
                         "inlets": "[]",
+                        "inlets_list": [],
                         "mapped": false,
                         "multiple_outputs": false,
                         "operator_class": "_PythonDecoratedOperator",
                         "operator_class_path": "{{ 
result.endswith('decorators.python._PythonDecoratedOperator') }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
@@ -264,11 +266,13 @@
                         "downstream_task_ids": "['check_events']",
                         "executor_config": {},
                         "inlets": "[]",
+                        "inlets_list": [],
                         "mapped": false,
                         "multiple_outputs": false,
                         "operator_class": "_PythonDecoratedOperator",
                         "operator_class_path": "{{ 
result.endswith('decorators.python._PythonDecoratedOperator') }}",
                         "outlets": "[]",
+                        "outlets_list": [],
                         "owner": "airflow",
                         "priority_weight": 1,
                         "queue": "{{ result is string }}",
diff --git a/providers/openlineage/tests/unit/openlineage/utils/test_utils.py 
b/providers/openlineage/tests/unit/openlineage/utils/test_utils.py
index 4078993937e..1b66b80ab5a 100644
--- a/providers/openlineage/tests/unit/openlineage/utils/test_utils.py
+++ b/providers/openlineage/tests/unit/openlineage/utils/test_utils.py
@@ -18,7 +18,9 @@
 from __future__ import annotations
 
 import datetime
+import enum
 import pathlib
+from decimal import Decimal
 from types import SimpleNamespace
 from unittest import mock
 from unittest.mock import MagicMock, PropertyMock, patch
@@ -33,7 +35,7 @@ from uuid6 import uuid7
 from airflow import DAG
 from airflow.models.dagrun import DagRun
 from airflow.models.taskinstance import TaskInstance, TaskInstanceState
-from airflow.providers.common.compat.assets import Asset
+from airflow.providers.common.compat.assets import Asset, AssetAlias
 from airflow.providers.common.compat.sdk import BaseOperator, TaskGroup, task, 
timezone
 from airflow.providers.openlineage.conf import namespace
 from airflow.providers.openlineage.plugins.facets import AirflowDagRunFacet, 
AirflowJobFacet
@@ -49,6 +51,7 @@ from airflow.providers.openlineage.utils.utils import (
     TaskInstanceInfo,
     TaskInstancePosition,
     _build_labeled_edge_map,
+    _convert_to_json_safe_list,
     _extract_ol_info_from_asset_event,
     _get_ol_job_dependencies_from_asset_events,
     _get_openlineage_data_from_dagrun_conf,
@@ -3400,6 +3403,7 @@ def test_task_info_af3():
         "hitl_summary": "hitl_summary",
         "ignore_first_depends_on_past": False,
         "inlets": "[{'uri': 'uri1', 'extra': {'a': 1}, 'type': 'asset'}]",
+        "inlets_list": [{"uri": "uri1", "extra": {"a": 1}, "type": "asset"}],
         "mapped": False,
         "max_active_tis_per_dag": None,
         "max_active_tis_per_dagrun": None,
@@ -3410,6 +3414,10 @@ def test_task_info_af3():
         "operator_class_path": get_fully_qualified_class_name(task_10),
         "operator_provider_version": None,  # Custom operator doesn't have 
provider version
         "outlets": "[{'uri': 'uri2', 'extra': {'b': 2}, 'type': 'asset'}, 
{'uri': 'uri3', 'extra': {'c': 3}, 'type': 'asset'}]",
+        "outlets_list": [
+            {"uri": "uri2", "extra": {"b": 2}, "type": "asset"},
+            {"uri": "uri3", "extra": {"c": 3}, "type": "asset"},
+        ],
         "owner": "airflow",
         "priority_weight": 1,
         "queue": "default",
@@ -3532,6 +3540,7 @@ def test_task_info_af2():
         "is_teardown": False,
         "sla": None,
         "inlets": "[{'uri': 'uri1', 'extra': {'a': 1}, 'type': 'asset'}]",
+        "inlets_list": [{"uri": "uri1", "extra": {"a": 1}, "type": "asset"}],
         "mapped": False,
         "max_active_tis_per_dag": None,
         "max_active_tis_per_dagrun": None,
@@ -3541,6 +3550,10 @@ def test_task_info_af2():
         "operator_class_path": get_fully_qualified_class_name(task_10),
         "operator_provider_version": None,  # Custom operator doesn't have 
provider version
         "outlets": "[{'uri': 'uri2', 'extra': {'b': 2}, 'type': 'asset'}, 
{'uri': 'uri3', 'extra': {'c': 3}, 'type': 'asset'}]",
+        "outlets_list": [
+            {"uri": "uri2", "extra": {"b": 2}, "type": "asset"},
+            {"uri": "uri3", "extra": {"c": 3}, "type": "asset"},
+        ],
         "owner": "airflow",
         "priority_weight": 1,
         "queue": "default",
@@ -5235,6 +5248,93 @@ class TestTaskInfoRuntimeAssets:
         assert result["outlets"] == "[{'uri': 's3://x/y.txt', 'extra': {'a': 
1}, 'type': 'asset_event'}]"
 
 
+class TestTaskInfoAssetLists:
+    """``inlets_list`` / ``outlets_list`` carry the same assets as ``inlets`` 
/ ``outlets`` as real lists."""
+
+    def test_extra_values_that_break_repr_parsing_stay_valid(self):
+        extra = {"etag": '"0x8D0000000000001"', "owner": "O'Brien", 
"has_header": True, "note": None}
+        task = BashOperator(
+            task_id="t",
+            bash_command="echo 1",
+            inlets=[Asset(uri="s3://bucket/in.csv", extra={"k": "v"})],
+            outlets=[Asset(uri="s3://bucket/out.csv", extra=extra)],
+        )
+        info = TaskInfo(task)
+        assert info["inlets_list"] == [{"uri": "s3://bucket/in.csv", "extra": 
{"k": "v"}, "type": "asset"}]
+        assert info["outlets_list"] == [{"uri": "s3://bucket/out.csv", 
"extra": extra, "type": "asset"}]
+
+    def test_no_assets_gives_empty_lists(self):
+        info = TaskInfo(BashOperator(task_id="t", bash_command="echo 1"))
+        assert info["inlets_list"] == []
+        assert info["outlets_list"] == []
+
+    def test_runtime_assets_included(self):
+        task = BashOperator(task_id="t", bash_command="echo 1", outlets=[])
+        dyn = Asset(
+            uri="s3://bucket/dyn.csv", extra={"size": Decimal("10.5"), "day": 
datetime.date(2026, 1, 2)}
+        )
+        aliased = Asset(uri="s3://bucket/aliased.csv", extra={"checks": 
[{"passed": True}]})
+        info = TaskInfo(task, runtime_assets=[(dyn, None), (aliased, 
AssetAlias("my-alias"))])
+        assert info["outlets_list"] == [
+            {
+                "uri": "s3://bucket/dyn.csv",
+                "extra": {"size": "10.5", "day": "2026-01-02"},
+                "type": "asset_event",
+            },
+            {
+                "uri": "s3://bucket/aliased.csv",
+                "extra": {"checks": [{"passed": True}]},
+                "type": "asset_event_from_alias",
+                "source_alias": "my-alias",
+            },
+        ]
+        assert info["inlets_list"] == []
+
+    def test_task_info_complete_includes_lists(self):
+        task = BashOperator(
+            task_id="t", bash_command="echo 1", 
outlets=[Asset(uri="s3://bucket/out.csv", extra={"k": "v"})]
+        )
+        assert TaskInfoComplete(task)["outlets_list"] == [
+            {"uri": "s3://bucket/out.csv", "extra": {"k": "v"}, "type": 
"asset"}
+        ]
+
+
+class _DatasetName(str, enum.Enum):
+    SYNC = "user_sync"
+
+
[email protected](
+    ("value", "expected"),
+    [
+        pytest.param([], [], id="empty"),
+        pytest.param(
+            [{("tuple", "key"): 1, "uri": "s3://b/o"}], [{"uri": "s3://b/o"}], 
id="non_str_key_dropped"
+        ),
+        pytest.param([{"uri": _DatasetName.SYNC}], [{"uri": "user_sync"}], 
id="str_enum_uses_value"),
+        pytest.param([{"n": Decimal("1.5")}], [{"n": "1.5"}], id="decimal"),
+        pytest.param(
+            [{"ts": datetime.datetime(2026, 1, 2, 3, 4, 5)}], [{"ts": 
"2026-01-02 03:04:05"}], id="datetime"
+        ),
+        pytest.param(
+            [{"nested": {"items": ["a", None, 1.0]}}], [{"nested": {"items": 
["a", None, 1.0]}}], id="nested"
+        ),
+    ],
+)
+def test_convert_to_json_safe_list(value, expected):
+    assert _convert_to_json_safe_list(value, "outlets") == expected
+
+
+@patch("airflow.providers.openlineage.utils.utils.log", autospec=True)
+def test_convert_to_json_safe_list_returns_none_on_failure(mock_log):
+    circular: list = []
+    circular.append(circular)
+    assert _convert_to_json_safe_list(circular, "outlets") is None
+    mock_log.warning.assert_called_once_with(
+        "Unable to serialize task `%s` to a JSON list: %s", "outlets", mock.ANY
+    )
+    assert str(mock_log.warning.call_args.args[2]) == "Circular reference 
detected"
+
+
 @patch("airflow.providers.openlineage.utils.utils.get_runtime_outlet_assets")
 @patch("airflow.providers.openlineage.conf.include_full_task_info")
 def test_get_airflow_run_facet_plumbs_runtime_assets(mock_include_full, 
mock_get_runtime):

Reply via email to