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

List of arbitrary elements?

Open larsks opened this issue 5 months ago • 0 comments

I'm trying to model the <extensions>...</extensions> element of the gpx schema, which accepts a list of "any elements from a namespace other than this schema's namespace". For example, something like:

<gpx xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.topografix.com/GPX/1/1">
  <waypoint lat="..." lon="...">
    <extensions>
      <foo:name xmlns:foo="https://foo.example.com/">Alice Example</foo:name>
      <bar:feature_type xmlns:bar="https://bar.example.com/">House</bar:title>
      <foo:display_address valid="1" xmlns:foo="https://foo.example.com/">1 Main St, Sometown, Somewhere 12345</foo:display_address>
    </extensions>
  </waypoint>
</gpx>

The <extensions> element needs to accept elements from arbitrary namespaces -- that is, client code may want to add its own extensions, and the module implementing the GPX spec doesn't know anything about them. This is a collection of heterogenous elements of unknown size.

I tried this:

from typing import Literal
from pydantic_xml import BaseXmlModel, attr, element


class GpxExtension(BaseXmlModel):
    pass


class FooNameExtension(
    GpxExtension, nsmap={"foo": "https://foo.example.com/"}, ns="foo", tag="name"
):
    name: str | None = None


class FooDisplayAddressExtension(
    GpxExtension,
    nsmap={"foo": "https://foo.example.com/"},
    ns="foo",
    tag="display_address",
):
    address: str | None = None
    valid: int | None = attr(default=None)


class BarFeatureTypeExtension(
    GpxExtension,
    nsmap={"foo": "https://foo.example.com/"},
    ns="foo",
    tag="feature_type",
):
    label: str | None = None


class Waypoint(BaseXmlModel):
    lat: float | None = attr(default=None)
    lon: float | None = attr(default=None)

    extensions: list[GpxExtension] = element(default_factory=list)


class GpxFile(
    BaseXmlModel,
    tag="gpx",
    nsmap={
        "xsi": "http://www.w3.org/2001/XMLSchema-instance",
        "": "http://www.topografix.com/GPX/1/1",
    },
):
    schemaLocation: Literal[
        "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
    ] = attr(
        default="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd",
        name="schemaLocation",
        ns="xsi",
    )
    version: Literal["1.1"] = attr(default="1.1")
    wpt: list[Waypoint] = []


w = Waypoint()
w.extensions.append(FooNameExtension(name="Alice Example"))
w.extensions.append(
    FooDisplayAddressExtension(address="1 Main St., Sometown, Somewhere 12345")
)
w.extensions.append(BarFeatureTypeExtension(label="House"))
g = GpxFile(wpt=[w])

print(g.to_xml(pretty_print=True).decode())

But that gets me:

<gpx xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.topografix.com/GPX/1/1" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" version="1.1">
  <wpt lat="" lon="">
    <extensions/>
    <extensions/>
    <extensions/>
  </wpt>
</gpx>

Although the individual elements serialize as expected:

>>> print(FooNameExtension(name='Alice Example').to_xml().decode())
<foo:name xmlns:foo="https://foo.example.com/">Alice Example</foo:name>

What's the right way to support a collection of arbitrary elements?

larsks avatar Oct 01 '24 14:10 larsks