Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 17 additions & 21 deletions .generator/src/generator/templates/model_utils.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1675,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,
Expand All @@ -1695,28 +1697,22 @@ 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
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]
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)
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]
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:
Expand Down
36 changes: 15 additions & 21 deletions src/datadog_api_client/model_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Comment thread
ddadamhooper marked this conversation as resolved.
return False


Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -1707,28 +1707,22 @@ 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
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]
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)
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]
if not any(item._unparsed for item in list_oneof_instance):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve empty oneOf arrays

When model_arg is an empty array, list_oneof_instance is also empty and any(...) is false, so this records the array branch even though non-list branches are still evaluated with empty model_kwargs. For datadog_api_client.v2.model.any_value.AnyValue (AnyValueObject plus [AnyValueItem]), deserializing a valid JSON value [] now records both the object and array matches and falls through to UnparsedObject, marking fields that use AnyValue as unparsed. Skip non-list branches for list payloads or special-case empty arrays so the array branch can be returned without ambiguity.

Useful? React with 👍 / 👎.

oneof_instances.append(list_oneof_instance)
elif issubclass(oneof_class, ModelSimple):
if model_arg is not None:
Expand Down
77 changes: 77 additions & 0 deletions tests/test_deserialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -339,3 +348,71 @@ 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


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 == []
Loading