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: 38 additions & 0 deletions tests/test_api_client/test_deserializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,44 @@ class Model:
deserialize_model(Model, data, model_finder=None)


def test_deserialize_model_tolerates_unknown_enum():
# A generated model whose setter rejects unknown enum values, mimicking the
# real SDK models (Contact.tax_number_type, LinkedTransaction...). The Xero
# API can return values the SDK predates, e.g. "TAXNUMBERTYPE/SSN" (#203,
# #205, #206) — deserialization must not crash on them.
class Model:
openapi_types = {"tax_number_type": "str"}
attribute_map = {"tax_number_type": "TaxNumberType"}
allowed_values = ["SSN", "EIN"]

def __init__(self, tax_number_type=None):
self._tax_number_type = None
if tax_number_type is not None:
self.tax_number_type = tax_number_type

@property
def tax_number_type(self):
return self._tax_number_type

@tax_number_type.setter
def tax_number_type(self, value):
if value and value not in self.allowed_values:
raise ValueError("Invalid value for `tax_number_type` ({})".format(value))
self._tax_number_type = value

with mock_deserialize():
result = deserialize_model(
Model, {"TaxNumberType": "TAXNUMBERTYPE/SSN"}, model_finder=None
)
# tolerated: no crash, raw value preserved for the caller
assert result.tax_number_type == "TAXNUMBERTYPE/SSN"

# control: a valid value still passes through the setter unchanged
with mock_deserialize():
ok = deserialize_model(Model, {"TaxNumberType": "EIN"}, model_finder=None)
assert ok.tax_number_type == "EIN"


class Shape(Enum):
"""
Test enum class to mimic Enum API model
Expand Down
16 changes: 15 additions & 1 deletion xero_python/api_client/deserializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,5 +284,19 @@ def deserialize_model(model, data, model_finder):
value = data[attr_key]
kwargs[attr] = deserialize(attr_type, value, model_finder)

instance = model(**kwargs)
try:
instance = model(**kwargs)
except ValueError:
# The Xero API can return enum values this generated SDK predates
# (e.g. new/unexpected tax_number_type or source_transaction_type_code
# values). The per-attribute setters reject those with ValueError, which
# otherwise crashes deserialization of an otherwise-valid response and
# forces callers to monkey-patch the SDK. Build the model tolerantly
# instead, preserving the raw value the API sent. (#203, #205, #206)
instance = model()
for attr, value in kwargs.items():
try:
setattr(instance, attr, value)
except ValueError:
setattr(instance, "_" + attr, value)
return instance