dapper91 / pydantic-xml

python xml for humans
https://pydantic-xml.readthedocs.io
The Unlicense
141 stars 14 forks source link

unable to load non-computed fields that come after computed fields #174

Closed mscarey closed 3 months ago

mscarey commented 3 months ago

I'm trying to parse some XML that has some fields computed from other fields. The problem is that some of the computed fields in this XML come before some of the non-computed fields. Pydantic-xml (version 2.9.0) doesn't seem to be able to see all the non-computed fields in this situation.

In this example, test_make_user will fail because it tries to load XML with the computed field greeting before the non-computed field name. But test_make_user_name_first passes.

from pydantic_xml import BaseXmlModel, computed_element, element

class User(BaseXmlModel):
    num: int = element()
    name: str = element()

    @computed_element
    @property
    def greeting(self) -> str:
        return f"Hello, {self.name}"

class TestPydantic:
    def test_make_user(self):
        example = """
            <User>
                <num>123</num>
                <greeting>Hello, John Doe</greeting>
                <name>John Doe</name>
            </User>
            """

        user = User.from_xml(example)
        assert user.num == 123
        assert user.name == "John Doe"
        assert user.greeting == "Hello, John Doe"

    def test_make_user_name_first(self):
        example = """
            <User>
                <num>123</num>
                <name>John Doe</name>
                <greeting>Hello, John Doe</greeting>
            </User>
            """

        user = User.from_xml(example)
        assert user.num == 123
        assert user.name == "John Doe"
        assert user.greeting == "Hello, John Doe"

The error for test_make_user looks like this:

test_pydantic.py::TestPydantic::test_make_user failed: self = <pydantic_xml.serializers.factories.model.ModelSerializer object at 0x1037fe5d0>
element = <pydantic_xml.element.native.lxml.XmlElement object at 0x103796f80>

    def deserialize(
            self,
            element: Optional[XmlElementReader],
            *,
            context: Optional[Dict[str, Any]],
            sourcemap: Dict[Location, int],
            loc: Location,
    ) -> Optional['pxml.BaseXmlModel']:
        if element is None:
            return None

        result: Dict[str, Any] = {}
        field_errors: Dict[Union[None, str, int], pd.ValidationError] = {}
        for field_name, field_serializer in self._field_serializers.items():
            try:
                loc = (field_name,)
                sourcemap[loc] = element.get_sourceline()
                field_value = field_serializer.deserialize(element, context=context, sourcemap=sourcemap, loc=loc)
                if field_value is not None:
                    field_name = self._fields_validation_aliases.get(field_name, field_name)
                    result[field_name] = field_value
            except pd.ValidationError as err:
                field_errors[field_name] = err

        if field_errors:
            raise utils.build_validation_error(title=self._model.__name__, errors_map=field_errors)

        if self._model.model_config.get('extra', 'ignore') == 'forbid':
            self._check_extra(self._model.__name__, element)

        try:
>           return self._model.model_validate(result, strict=False, context=context)
E           pydantic_core._pydantic_core.ValidationError: 1 validation error for User
E           name
E             Field required [type=missing, input_value={'num': '123'}, input_type=dict]
E               For further information visit https://errors.pydantic.dev/2.6/v/missing

venv/lib/python3.11/site-packages/pydantic_xml/serializers/factories/model.py:207: ValidationError

During handling of the above exception, another exception occurred:

self = <unit.test_pydantic.TestPydantic object at 0x1037ebbd0>

    def test_make_user(self):
        example = """
            <User>
                <num>123</num>
                <greeting>Hello, John Doe</greeting>
                <name>John Doe</name>
            </User>
            """

>       user = User.from_xml(example)

tests/unit/test_pydantic.py:24: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
venv/lib/python3.11/site-packages/pydantic_xml/model.py:402: in from_xml
    return cls.from_xml_tree(etree.fromstring(source), context=context)
venv/lib/python3.11/site-packages/pydantic_xml/model.py:379: in from_xml_tree
    ModelT, cls.__xml_serializer__.deserialize(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pydantic_xml.serializers.factories.model.ModelSerializer object at 0x1037fe5d0>
element = <pydantic_xml.element.native.lxml.XmlElement object at 0x103796f80>

    def deserialize(
            self,
            element: Optional[XmlElementReader],
            *,
            context: Optional[Dict[str, Any]],
            sourcemap: Dict[Location, int],
            loc: Location,
    ) -> Optional['pxml.BaseXmlModel']:
        if element is None:
            return None

        result: Dict[str, Any] = {}
        field_errors: Dict[Union[None, str, int], pd.ValidationError] = {}
        for field_name, field_serializer in self._field_serializers.items():
            try:
                loc = (field_name,)
                sourcemap[loc] = element.get_sourceline()
                field_value = field_serializer.deserialize(element, context=context, sourcemap=sourcemap, loc=loc)
                if field_value is not None:
                    field_name = self._fields_validation_aliases.get(field_name, field_name)
                    result[field_name] = field_value
            except pd.ValidationError as err:
                field_errors[field_name] = err

        if field_errors:
            raise utils.build_validation_error(title=self._model.__name__, errors_map=field_errors)

        if self._model.model_config.get('extra', 'ignore') == 'forbid':
            self._check_extra(self._model.__name__, element)

        try:
            return self._model.model_validate(result, strict=False, context=context)
        except pd.ValidationError as err:
>           raise utils.set_validation_error_sourceline(err, sourcemap)
E           pydantic_core._pydantic_core.ValidationError: 1 validation error for User
E           name
E             [line 2]: Field required [type=missing, input_value={'num': '123'}, input_type=dict]

venv/lib/python3.11/site-packages/pydantic_xml/serializers/factories/model.py:209: ValidationError
dapper91 commented 3 months ago

@mscarey Hi,

This error is caused not by the computed field. The library uses strict parsing mode by default, which means it respects elements order. You can find more info here. So in your case it expects element name but finds greeting. In the second case it just ignores the last element (greeting). To fix the problem use ordered search mode:

class User(BaseXmlModel, search_mode='ordered'):
    num: int = element()
    name: str = element()

    @computed_element
    @property
    def greeting(self) -> str:
        return f"Hello, {self.name}"
mscarey commented 3 months ago

Thanks so much for the help! Sorry to raise a false alarm.