diff --git a/msrest/serialization.py b/msrest/serialization.py index 0e65d8e5c8..11f751698e 100644 --- a/msrest/serialization.py +++ b/msrest/serialization.py @@ -336,8 +336,8 @@ def from_dict(cls, data, key_extractors=None, content_type=None): """ deserializer = Deserializer(cls._infer_class_models()) deserializer.key_extractors = [ - rest_key_case_insensitive_extractor, attribute_key_case_insensitive_extractor, + rest_key_case_insensitive_extractor, last_rest_key_case_insensitive_extractor ] if key_extractors is None else key_extractors return deserializer(cls.__name__, data, content_type=content_type) @@ -1160,11 +1160,17 @@ def rest_key_case_insensitive_extractor(attr, attr_desc, data): return attribute_key_case_insensitive_extractor(key, None, working_data) def last_rest_key_extractor(attr, attr_desc, data): + """Extract the attribute in "data" based on the last part of the JSON path key. + """ key = attr_desc['key'] dict_keys = _FLATTEN.split(key) return attribute_key_extractor(dict_keys[-1], None, data) def last_rest_key_case_insensitive_extractor(attr, attr_desc, data): + """Extract the attribute in "data" based on the last part of the JSON path key. + + This is the case insensitive version of "last_rest_key_extractor" + """ key = attr_desc['key'] dict_keys = _FLATTEN.split(key) return attribute_key_case_insensitive_extractor(dict_keys[-1], None, data) @@ -1259,8 +1265,8 @@ def xml_key_extractor(attr, attr_desc, data): class Deserializer(object): """Response object model deserializer. - :param dict classes: Class type dictionary for deserializing - complex types. + :param dict classes: Class type dictionary for deserializing complex types. + :ivar list key_extractors: Ordered list of extractors to be used by this deserializer. """ basic_types = {str: 'str', int: 'int', bool: 'bool', float: 'float'} @@ -1375,7 +1381,15 @@ def _deserialize(self, target_obj, data): found_value = key_extractor(attr, attr_desc, data) if found_value is not None: if raw_value is not None and raw_value != found_value: - raise KeyError('Use twice the key: "{}"'.format(attr)) + msg = ("Ignoring extracted value '%s' from %s for key '%s'" + " (duplicate extraction, follow extractors order)" ) + _LOGGER.warning( + msg, + found_value, + key_extractor, + attr + ) + continue raw_value = found_value value = self.deserialize_data(raw_value, attr_desc['type']) diff --git a/tests/test_serialization.py b/tests/test_serialization.py index fffd8a9b36..5991753631 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -1436,6 +1436,33 @@ def assert_model(inst): self.TestObj.from_dict(attr_data) assert_model(model_instance) + def test_twice_key_scenario(self): + # Te reproduce the initial bug, you need a attribute named after the last part + # of a flattening JSON from another attribute (here type) + # https://github.com/Azure/azure-sdk-for-python/issues/11422 + # Issue happend where searching for "type2", since we found a match in both "type2" and "type" keys + + class LocalModel(Model): + _attribute_map = { + 'id': {'key': 'id', 'type': 'int'}, + 'type': {'key': 'type_dont_matter_not_used', 'type': 'str'}, + 'type2': {'key': 'properties.type', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(LocalModel, self).__init__(**kwargs) + + raw = { + 'id': 42, + 'type': "type", + 'type2': "type2" + } + + m = LocalModel.from_dict(raw) + assert m.id == 42 + assert m.type == "type" + assert m.type2 == "type2" + def test_array_deserialize(self): result = self.d('[str]', ["a","b"]) assert result == ['a','b'] @@ -1503,13 +1530,21 @@ class TestKeyTypeObj(Model): self.assertEqual(3, obj.attr_c) self.assertEqual(4, obj.attr_d) - with self.assertRaises(DeserializationError): - obj = TestKeyTypeObj.from_dict({ - "attr_b": 1, - "id": 2, - "keyc": 3, - "keyd": 4 - }) + # This one used to raise an exception, but after https://github.com/Azure/msrest-for-python/pull/204 + # we decide to accept it with log warning + + obj = TestKeyTypeObj.from_dict({ + "attr_a": 1, + "attr_b": 12, # Conflict with "id" + "id": 14, # Conflict with "attr_b" + "keyc": 3, + "keyd": 4 + }) + + self.assertEqual(1, obj.attr_a) + self.assertEqual(12, obj.attr_b) # from_dict will prioritize attribute syntax + self.assertEqual(3, obj.attr_c) + self.assertEqual(4, obj.attr_d) def test_basic_deserialization(self): class TestObj(Model):