From e0bad42583202636910092cce97bfd37aac95db1 Mon Sep 17 00:00:00 2001 From: Adam Hooper Date: Fri, 10 Jul 2026 15:23:56 -0400 Subject: [PATCH 1/6] Fix oneOf deserialization for composed models with a list branch Two related deserialization bugs surfaced when parsing the topology_map and geomap dashboard widgets: - ModelNormal.set_attribute only propagated _unparsed from list elements that were UnparsedObject instances, missing nested OpenApiModel instances that were themselves _unparsed. A topology_map request whose query carried a mismatched data_source enum could ambiguously satisfy both oneOf variants without being flagged. - allows_single_value_input skipped list oneOf branches, returning False for a ModelComposed such as FormulaAndFunctionEventQueryGroupByConfig (oneOf of a group-by list or a flat fields object). deserialize_model then splatted a list input into positional args, crashing ModelComposed.__init__; the TypeError was swallowed during matching and masked the failure once _unparsed propagation was fixed. Propagate _unparsed from nested models in lists, and treat a list oneOf branch as allowing single-value input so list inputs route through get_oneof_instance. Applied to both the generated model_utils.py and the generator template. --- .generator/src/generator/templates/model_utils.j2 | 6 ++++-- src/datadog_api_client/model_utils.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.generator/src/generator/templates/model_utils.j2 b/.generator/src/generator/templates/model_utils.j2 index b0d701eaab..ea3a1f4770 100644 --- a/.generator/src/generator/templates/model_utils.j2 +++ b/.generator/src/generator/templates/model_utils.j2 @@ -83,7 +83,9 @@ def allows_single_value_input(cls): elif issubclass(cls, ModelComposed): if not cls._composed_schemas["oneOf"]: return False - return any(allows_single_value_input(c) for c in cls._composed_schemas["oneOf"] if not isinstance(c, list)) + return any( + isinstance(c, list) or allows_single_value_input(c) for c in cls._composed_schemas["oneOf"] + ) return False @@ -169,7 +171,7 @@ class OpenApiModel: ) if isinstance(value, list): for x in value: - if isinstance(x, UnparsedObject): + if isinstance(x, UnparsedObject) or (isinstance(x, OpenApiModel) and x._unparsed): self._unparsed = True if name in self.validations: check_validations(self.validations[name], name, value, self._configuration) diff --git a/src/datadog_api_client/model_utils.py b/src/datadog_api_client/model_utils.py index 9f508e1f57..2e10532ec1 100644 --- a/src/datadog_api_client/model_utils.py +++ b/src/datadog_api_client/model_utils.py @@ -84,7 +84,7 @@ def allows_single_value_input(cls): elif issubclass(cls, ModelComposed): if not cls._composed_schemas["oneOf"]: return False - return any(allows_single_value_input(c) for c in cls._composed_schemas["oneOf"] if not isinstance(c, list)) + return any(isinstance(c, list) or allows_single_value_input(c) for c in cls._composed_schemas["oneOf"]) return False @@ -181,7 +181,7 @@ def set_attribute(self, name, value): ) if isinstance(value, list): for x in value: - if isinstance(x, UnparsedObject): + if isinstance(x, UnparsedObject) or (isinstance(x, OpenApiModel) and x._unparsed): self._unparsed = True if name in self.validations: check_validations(self.validations[name], name, value, self._configuration) From 4262243d5e19f28f1a3b492e018a5ff9de262608 Mon Sep 17 00:00:00 2001 From: Adam Hooper Date: Mon, 13 Jul 2026 10:45:47 -0400 Subject: [PATCH 2/6] Fail on list with some-valid, some-invalid elements --- .../src/generator/templates/model_utils.j2 | 31 +++++++++---------- src/datadog_api_client/model_utils.py | 31 +++++++++---------- 2 files changed, 28 insertions(+), 34 deletions(-) diff --git a/.generator/src/generator/templates/model_utils.j2 b/.generator/src/generator/templates/model_utils.j2 index ea3a1f4770..f583564635 100644 --- a/.generator/src/generator/templates/model_utils.j2 +++ b/.generator/src/generator/templates/model_utils.j2 @@ -1677,16 +1677,16 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None): else: if isinstance(oneof_class, list): oneof_class = oneof_class[0] - list_oneof_instance = [] if model_arg is None and not model_kwargs: # Empty data - oneof_instances.append(list_oneof_instance) + oneof_instances.append([]) continue # Check if inner type is primitive - follows same pattern as complex objects: # https://github.com/DataDog/datadog-api-client-python/blob/008536d34760ce096e118edc54df613d82194529/.generator/src/generator/templates/model_utils.j2#L1590-L1599 if oneof_class in PRIMITIVE_TYPES: # Handle list of primitives (e.g., [str], [float]) + list_oneof_instance = [] for arg in model_arg: oneof_instance = validate_and_convert_types( arg, @@ -1701,24 +1701,21 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None): oneof_instances.append(list_oneof_instance) elif inspect.isclass(oneof_class) and issubclass(oneof_class, ModelSimple): # Handle list of ModelSimple - for arg in model_arg: - oneof_instance = oneof_class(arg, **constant_kwargs) - if not oneof_instance._unparsed: - list_oneof_instance.append(oneof_instance) - if list_oneof_instance: + list_oneof_instance = [oneof_class(arg, **constant_kwargs) for arg in model_arg] + # Reject the branch unless every element parsed. + if list_oneof_instance and not any(item._unparsed for item in list_oneof_instance): oneof_instances.append(list_oneof_instance) else: # Handle list of complex objects (ModelNormal, ModelComposed) - for arg in model_arg: - if constant_kwargs.get("_spec_property_naming"): - oneof_instance = oneof_class( - **change_keys_js_to_python(arg, oneof_class), **constant_kwargs - ) - else: - oneof_instance = oneof_class(**arg, **constant_kwargs) - if not oneof_instance._unparsed: - list_oneof_instance.append(oneof_instance) - if list_oneof_instance: + if constant_kwargs.get("_spec_property_naming"): + list_oneof_instance = [ + oneof_class(**change_keys_js_to_python(arg, oneof_class), **constant_kwargs) + for arg in model_arg + ] + else: + list_oneof_instance = [oneof_class(**arg, **constant_kwargs) for arg in model_arg] + # Reject the branch unless every element parsed. + if list_oneof_instance and not any(item._unparsed for item in list_oneof_instance): oneof_instances.append(list_oneof_instance) elif issubclass(oneof_class, ModelSimple): if model_arg is not None: diff --git a/src/datadog_api_client/model_utils.py b/src/datadog_api_client/model_utils.py index 2e10532ec1..c9b956a13d 100644 --- a/src/datadog_api_client/model_utils.py +++ b/src/datadog_api_client/model_utils.py @@ -1687,16 +1687,16 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None): else: if isinstance(oneof_class, list): oneof_class = oneof_class[0] - list_oneof_instance = [] if model_arg is None and not model_kwargs: # Empty data - oneof_instances.append(list_oneof_instance) + oneof_instances.append([]) continue # Check if inner type is primitive - follows same pattern as complex objects: # https://github.com/DataDog/datadog-api-client-python/blob/008536d34760ce096e118edc54df613d82194529/.generator/src/generator/templates/model_utils.j2#L1590-L1599 if oneof_class in PRIMITIVE_TYPES: # Handle list of primitives (e.g., [str], [float]) + list_oneof_instance = [] for arg in model_arg: oneof_instance = validate_and_convert_types( arg, @@ -1711,24 +1711,21 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None): oneof_instances.append(list_oneof_instance) elif inspect.isclass(oneof_class) and issubclass(oneof_class, ModelSimple): # Handle list of ModelSimple - for arg in model_arg: - oneof_instance = oneof_class(arg, **constant_kwargs) - if not oneof_instance._unparsed: - list_oneof_instance.append(oneof_instance) - if list_oneof_instance: + list_oneof_instance = [oneof_class(arg, **constant_kwargs) for arg in model_arg] + # Reject the branch unless every element parsed. + if list_oneof_instance and not any(item._unparsed for item in list_oneof_instance): oneof_instances.append(list_oneof_instance) else: # Handle list of complex objects (ModelNormal, ModelComposed) - for arg in model_arg: - if constant_kwargs.get("_spec_property_naming"): - oneof_instance = oneof_class( - **change_keys_js_to_python(arg, oneof_class), **constant_kwargs - ) - else: - oneof_instance = oneof_class(**arg, **constant_kwargs) - if not oneof_instance._unparsed: - list_oneof_instance.append(oneof_instance) - if list_oneof_instance: + if constant_kwargs.get("_spec_property_naming"): + list_oneof_instance = [ + oneof_class(**change_keys_js_to_python(arg, oneof_class), **constant_kwargs) + for arg in model_arg + ] + else: + list_oneof_instance = [oneof_class(**arg, **constant_kwargs) for arg in model_arg] + # Reject the branch unless every element parsed. + if list_oneof_instance and not any(item._unparsed for item in list_oneof_instance): oneof_instances.append(list_oneof_instance) elif issubclass(oneof_class, ModelSimple): if model_arg is not None: From 326bb385fdb4c393a91e964eb026a84317799236 Mon Sep 17 00:00:00 2001 From: Adam Hooper Date: Mon, 13 Jul 2026 10:45:54 -0400 Subject: [PATCH 3/6] Add unit tests --- tests/test_deserialization.py | 66 +++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/test_deserialization.py b/tests/test_deserialization.py index 503bc0bdc4..4aeaee845d 100644 --- a/tests/test_deserialization.py +++ b/tests/test_deserialization.py @@ -13,6 +13,15 @@ from datadog_api_client.v2.model.logs_archive import LogsArchive from datadog_api_client.v2.model.logs_archive_destination import LogsArchiveDestination from datadog_api_client.v2.model.user_response import UserResponse +from datadog_api_client.v1.model.formula_and_function_event_query_definition import ( + FormulaAndFunctionEventQueryDefinition, +) +from datadog_api_client.v1.model.formula_and_function_event_query_group_by import ( + FormulaAndFunctionEventQueryGroupBy, +) +from datadog_api_client.v1.model.formula_and_function_event_query_group_by_config import ( + FormulaAndFunctionEventQueryGroupByConfig, +) def test_unknown_nested_oneof_in_list(): @@ -339,3 +348,60 @@ def test_schema_declared_float_still_upconverts_int_input(): an integer JSON value must still upconvert to float.""" converted = validate_and_convert_types(3, (float,), ["received_data"], True, True, Configuration()) assert type(converted) is float and converted == 3.0 + + +def test_one_of_list_branch_rejected_when_element_unparsed(): + """A oneOf list branch must reject the whole value when any element fails to + parse, rather than silently dropping the bad element and accepting a + truncated list. ``FormulaAndFunctionEventQueryGroupByConfig`` is a oneOf + whose first branch is ``[FormulaAndFunctionEventQueryGroupBy]``.""" + value = [ + {"facet": "@geo.country_iso_code", "sort": {"aggregation": "count", "order": "desc"}}, + # Unknown aggregation enum -> this element is unparsed. + {"facet": "@http.status_code", "sort": {"aggregation": "a non existent aggregation"}}, + ] + config = Configuration() + deserialized = validate_and_convert_types( + value, (FormulaAndFunctionEventQueryGroupByConfig,), ["received_data"], True, True, config + ) + # The whole value is unparsed rather than a one-element list of the valid item. + assert isinstance(deserialized, UnparsedObject) + + +def test_one_of_list_branch_all_valid_yields_objects(): + """When every element of a oneOf list branch parses, the value deserializes + to a list of model objects (not raw dicts).""" + config = Configuration() + + valid = [ + {"facet": "@geo.country_iso_code", "limit": 250, "sort": {"aggregation": "count", "order": "desc"}}, + ] + group_by = validate_and_convert_types( + valid, (FormulaAndFunctionEventQueryGroupByConfig,), ["received_data"], True, True, config + ) + assert isinstance(group_by, list) + assert isinstance(group_by[0], FormulaAndFunctionEventQueryGroupBy) + assert group_by[0].facet == "@geo.country_iso_code" + assert str(group_by[0].sort.order) == "desc" + + +def test_one_of_list_branch_unparsed_propagates_to_enclosing_model(): + """An unparsed element inside a oneOf list branch must propagate ``_unparsed`` + up to the containing model, rather than being silently dropped so the model + looks fully parsed.""" + config = Configuration() + body = { + "data_source": "logs", + "name": "my_query", + "compute": {"aggregation": "count"}, + "group_by": [ + {"facet": "@geo.country_iso_code", "sort": {"aggregation": "count", "order": "desc"}}, + # Unknown aggregation enum -> this element is unparsed. + {"facet": "@http.status_code", "sort": {"aggregation": "a non existent aggregation"}}, + ], + } + definition = validate_and_convert_types( + body, (FormulaAndFunctionEventQueryDefinition,), ["received_data"], True, True, config + ) + assert isinstance(definition, FormulaAndFunctionEventQueryDefinition) + assert definition._unparsed From 85501f861554f4e1dcfe1db66743fd53149f3854 Mon Sep 17 00:00:00 2001 From: Adam Hooper Date: Mon, 13 Jul 2026 14:35:02 -0400 Subject: [PATCH 4/6] Allow empty list --- .generator/src/generator/templates/model_utils.j2 | 9 +++------ src/datadog_api_client/model_utils.py | 9 +++------ tests/test_deserialization.py | 11 +++++++++++ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/.generator/src/generator/templates/model_utils.j2 b/.generator/src/generator/templates/model_utils.j2 index f583564635..d041d66edd 100644 --- a/.generator/src/generator/templates/model_utils.j2 +++ b/.generator/src/generator/templates/model_utils.j2 @@ -1697,13 +1697,11 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None): configuration=constant_kwargs.get("_configuration"), ) list_oneof_instance.append(oneof_instance) - if list_oneof_instance: - oneof_instances.append(list_oneof_instance) + oneof_instances.append(list_oneof_instance) elif inspect.isclass(oneof_class) and issubclass(oneof_class, ModelSimple): # Handle list of ModelSimple list_oneof_instance = [oneof_class(arg, **constant_kwargs) for arg in model_arg] - # Reject the branch unless every element parsed. - if list_oneof_instance and not any(item._unparsed for item in list_oneof_instance): + if not any(item._unparsed for item in list_oneof_instance): oneof_instances.append(list_oneof_instance) else: # Handle list of complex objects (ModelNormal, ModelComposed) @@ -1714,8 +1712,7 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None): ] else: list_oneof_instance = [oneof_class(**arg, **constant_kwargs) for arg in model_arg] - # Reject the branch unless every element parsed. - if list_oneof_instance and not any(item._unparsed for item in list_oneof_instance): + if not any(item._unparsed for item in list_oneof_instance): oneof_instances.append(list_oneof_instance) elif issubclass(oneof_class, ModelSimple): if model_arg is not None: diff --git a/src/datadog_api_client/model_utils.py b/src/datadog_api_client/model_utils.py index c9b956a13d..97a4407fd5 100644 --- a/src/datadog_api_client/model_utils.py +++ b/src/datadog_api_client/model_utils.py @@ -1707,13 +1707,11 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None): configuration=constant_kwargs.get("_configuration"), ) list_oneof_instance.append(oneof_instance) - if list_oneof_instance: - oneof_instances.append(list_oneof_instance) + oneof_instances.append(list_oneof_instance) elif inspect.isclass(oneof_class) and issubclass(oneof_class, ModelSimple): # Handle list of ModelSimple list_oneof_instance = [oneof_class(arg, **constant_kwargs) for arg in model_arg] - # Reject the branch unless every element parsed. - if list_oneof_instance and not any(item._unparsed for item in list_oneof_instance): + if not any(item._unparsed for item in list_oneof_instance): oneof_instances.append(list_oneof_instance) else: # Handle list of complex objects (ModelNormal, ModelComposed) @@ -1724,8 +1722,7 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None): ] else: list_oneof_instance = [oneof_class(**arg, **constant_kwargs) for arg in model_arg] - # Reject the branch unless every element parsed. - if list_oneof_instance and not any(item._unparsed for item in list_oneof_instance): + if not any(item._unparsed for item in list_oneof_instance): oneof_instances.append(list_oneof_instance) elif issubclass(oneof_class, ModelSimple): if model_arg is not None: diff --git a/tests/test_deserialization.py b/tests/test_deserialization.py index 4aeaee845d..274634d978 100644 --- a/tests/test_deserialization.py +++ b/tests/test_deserialization.py @@ -405,3 +405,14 @@ def test_one_of_list_branch_unparsed_propagates_to_enclosing_model(): ) assert isinstance(definition, FormulaAndFunctionEventQueryDefinition) assert definition._unparsed + + +def test_one_of_empty_list_branch_accepted(): + """An empty array for a oneOf list branch is valid: every element parses + vacuously, so the value deserializes to an empty list rather than falling + through to ``UnparsedObject`` and marking the enclosing model unparsed.""" + config = Configuration() + group_by = validate_and_convert_types( + [], (FormulaAndFunctionEventQueryGroupByConfig,), ["received_data"], True, True, config + ) + assert group_by == [] From 805bd2885bcc1ceba18f4bb542509235a3827e84 Mon Sep 17 00:00:00 2001 From: Adam Hooper Date: Tue, 14 Jul 2026 10:12:41 -0400 Subject: [PATCH 5/6] AnyValue: parse empty array correctly --- .generator/src/generator/templates/model_utils.j2 | 4 +++- src/datadog_api_client/model_utils.py | 4 +++- tests/test_deserialization.py | 9 +++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.generator/src/generator/templates/model_utils.j2 b/.generator/src/generator/templates/model_utils.j2 index d041d66edd..ec66f5f183 100644 --- a/.generator/src/generator/templates/model_utils.j2 +++ b/.generator/src/generator/templates/model_utils.j2 @@ -1665,7 +1665,9 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None): single_value_input = allows_single_value_input(oneof_class) if not isinstance(oneof_class, list) else True with suppress(Exception): - if not single_value_input: + if isinstance(model_class, list) and not isinstance(oneof_class, list): + continue + elif not single_value_input: if constant_kwargs.get("_spec_property_naming"): oneof_instance = oneof_class( **change_keys_js_to_python(model_kwargs, oneof_class), **constant_kwargs diff --git a/src/datadog_api_client/model_utils.py b/src/datadog_api_client/model_utils.py index 97a4407fd5..0044678b22 100644 --- a/src/datadog_api_client/model_utils.py +++ b/src/datadog_api_client/model_utils.py @@ -1675,7 +1675,9 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None): single_value_input = allows_single_value_input(oneof_class) if not isinstance(oneof_class, list) else True with suppress(Exception): - if not single_value_input: + if isinstance(model_class, list) and not isinstance(oneof_class, list): + continue + elif not single_value_input: if constant_kwargs.get("_spec_property_naming"): oneof_instance = oneof_class( **change_keys_js_to_python(model_kwargs, oneof_class), **constant_kwargs diff --git a/tests/test_deserialization.py b/tests/test_deserialization.py index 274634d978..422c439a9e 100644 --- a/tests/test_deserialization.py +++ b/tests/test_deserialization.py @@ -8,6 +8,7 @@ from datadog_api_client.v1.model.synthetics_api_test import SyntheticsAPITest from datadog_api_client.v1.model.synthetics_browser_test import SyntheticsBrowserTest from datadog_api_client.v1.model.synthetics_assertion import SyntheticsAssertion +from datadog_api_client.v2.model.any_value import AnyValue from datadog_api_client.v2.model.downtime_response import DowntimeResponse from datadog_api_client.v2.model.logs_aggregate_response import LogsAggregateResponse from datadog_api_client.v2.model.logs_archive import LogsArchive @@ -416,3 +417,11 @@ def test_one_of_empty_list_branch_accepted(): [], (FormulaAndFunctionEventQueryGroupByConfig,), ["received_data"], True, True, config ) assert group_by == [] + + +def test_one_of_empty_list_not_ambiguous_with_object_branch(): + """``AnyValue`` must match an empty array.""" + config = Configuration() + value = validate_and_convert_types([], (AnyValue,), ["received_data"], True, True, config) + assert value == [] + assert not isinstance(value, UnparsedObject) From 376d22332a1e81282cc8e1296e949e9ff7a95c61 Mon Sep 17 00:00:00 2001 From: Adam Hooper Date: Tue, 14 Jul 2026 10:38:04 -0400 Subject: [PATCH 6/6] Fix logic typo --- .generator/src/generator/templates/model_utils.j2 | 7 ++++--- src/datadog_api_client/model_utils.py | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.generator/src/generator/templates/model_utils.j2 b/.generator/src/generator/templates/model_utils.j2 index ec66f5f183..e091f010e8 100644 --- a/.generator/src/generator/templates/model_utils.j2 +++ b/.generator/src/generator/templates/model_utils.j2 @@ -1665,9 +1665,10 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None): single_value_input = allows_single_value_input(oneof_class) if not isinstance(oneof_class, list) else True with suppress(Exception): - if isinstance(model_class, list) and not isinstance(oneof_class, list): - continue - elif not single_value_input: + if not single_value_input: + if isinstance(model_arg, list): + # A list input cannot match a non-single-value (object) schema. + continue if constant_kwargs.get("_spec_property_naming"): oneof_instance = oneof_class( **change_keys_js_to_python(model_kwargs, oneof_class), **constant_kwargs diff --git a/src/datadog_api_client/model_utils.py b/src/datadog_api_client/model_utils.py index 0044678b22..1152fc3628 100644 --- a/src/datadog_api_client/model_utils.py +++ b/src/datadog_api_client/model_utils.py @@ -1675,9 +1675,10 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None): single_value_input = allows_single_value_input(oneof_class) if not isinstance(oneof_class, list) else True with suppress(Exception): - if isinstance(model_class, list) and not isinstance(oneof_class, list): - continue - elif not single_value_input: + if not single_value_input: + if isinstance(model_arg, list): + # A list input cannot match a non-single-value (object) schema. + continue if constant_kwargs.get("_spec_property_naming"): oneof_instance = oneof_class( **change_keys_js_to_python(model_kwargs, oneof_class), **constant_kwargs