pydantic-xml icon indicating copy to clipboard operation
pydantic-xml copied to clipboard

[Question] Combining multiple tags into a single attribute

Open synapticarbors opened this issue 11 months ago • 2 comments

Digging through the documentation and code, I was unable to figure out if the following was possible, and if so, what the best approach would be. Supposing I had an xml snippet that looked like:

<system>
    <A>
        <row>
            <item>1.0</item>
            <item>0.0</item>
            <item>0.0</item>
        </row>
        <row>
            <item>0.0</item>
            <item>2.2</item>
            <item>3.3</item>
        </row>
        <row>
            <item>0.0</item>
            <item>4.4</item>
            <item>5.5</item>
        </row>
    </A>
</system>

I'm trying to map A to something like:

class Example(BaseXmlModel, tag="system"):
    A: list[list[float]]: element(default_factory=list)

where ex = Example.from_xml(doc.xml) would have

ex.A == [[1.0, 0.0, 0.0], [0.0, 2.2, 3.3], [0.0, 4.4, 5.5]]

The xml_field_validator used in the custom xml serialization example only really seems capable of mapping one tag to a single attribute. Any suggestions on how to do this, as well as the reverse serialization step would be greatly appreciated.

synapticarbors avatar Mar 07 '25 19:03 synapticarbors

I'm not sure if there is a better way to solve this, but I just ended up explicitly modeling A, and then keeping a non-exporting field to hold the nested list:

import pydantic_xml as px
from pydantic_xml import BaseXmlModel


class Row(BaseXmlModel, tag='row'):
    items: list[float] = px.element(tag='item')


class Amat(BaseXmlModel, tag='A'):
    rows: list[Row] = px.element(tag='row')


class System(BaseXmlModel, tag="system"):
    A_data: Amat = px.element(tag='A')
    _A: list[list[float]] | None = None

    @property
    def A(self) -> list[list[float]]:
        if self._A is None:
            self._A = [[item for item in row.items] for row in self.A_data.rows]
        return self._A

    @A.setter
    def A(self, value: list[list[float]]):
        self.A_data = Amat(rows=[Row(items=row) for row in value])
        self._A = None   

synapticarbors avatar Mar 10 '25 01:03 synapticarbors

@synapticarbors Hi,

you can declare Row as a RootXmlModel and define __iter__ and __eq__ methods:

from typing import Iterator

import pydantic_xml as pxml

xml = '''
<system>
    <A>
        <row>
            <item>1.0</item>
            <item>0.0</item>
            <item>0.0</item>
        </row>
        <row>
            <item>0.0</item>
            <item>2.2</item>
            <item>3.3</item>
        </row>
        <row>
            <item>0.0</item>
            <item>4.4</item>
            <item>5.5</item>
        </row>
    </A>
</system>
'''

class Row(pxml.RootXmlModel):
    root: list[float] = pxml.element(tag='item')

    def __iter__(self) -> Iterator[float]:
        return iter(self.root)

    def __getitem__(self, item: int) -> float:
        return self.root[item]

    def __eq__(self, other: list[float]) -> bool:
        return self.root == other



class System(pxml.BaseXmlModel, tag="system"):
    a: list[Row] = pxml.wrapped("A", pxml.element(tag="row", default_factory=list))


sys = System.from_xml(xml)
assert sys.a == [[1.0, 0.0, 0.0], [0.0, 2.2, 3.3], [0.0, 4.4, 5.5]]

dapper91 avatar Mar 11 '25 16:03 dapper91