From 77ad05dc01ed853b7dcbbf33db2fb4297bac06ee Mon Sep 17 00:00:00 2001 From: CedricConday Date: Wed, 1 Jul 2026 11:16:01 +0000 Subject: [PATCH] fix: tolerate unknown enum values on deserialization (#203, #205, #206) The Xero API can return enum values this generated SDK predates (e.g. tax_number_type='TAXNUMBERTYPE/SSN', source_transaction_type_code='RECEIPT'). The per-attribute setters reject those with ValueError, which crashes deserialization of an otherwise-valid response and forces callers to monkey-patch the SDK. deserialize_model now falls back to building the model tolerantly, preserving the raw value the API sent. Valid values are unaffected. Adds a regression test. --- tests/test_api_client/test_deserializer.py | 38 ++++++++++++++++++++++ xero_python/api_client/deserializer.py | 16 ++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/tests/test_api_client/test_deserializer.py b/tests/test_api_client/test_deserializer.py index 9c851a8a..9dfcf160 100644 --- a/tests/test_api_client/test_deserializer.py +++ b/tests/test_api_client/test_deserializer.py @@ -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 diff --git a/xero_python/api_client/deserializer.py b/xero_python/api_client/deserializer.py index b64e9820..1ed8c4cd 100644 --- a/xero_python/api_client/deserializer.py +++ b/xero_python/api_client/deserializer.py @@ -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