Repository: tefra/xsdata
Pull request #975 (2024-03-11 10:04:07)

feat: Add xml and lxml tree serializers
## 📒 Description

> Write a brief description of your PR.

Resolves https://github.com/tefra/xsdata/issues/849

## 🔗 What I've Done

> Write a description of the steps taken to resolve the issue

## 💬 Comments

> A place to write any comments to the reviewer.

## 🛫 Checklist

- [x] Updated docs
- [x] Added unit-tests
- [x] [Sample tests](https://github.com/tefra/xsdata-samples) pass
- [x] [W3C tests](https://github.com/tefra/xsdata-w3c-tests) pass



Patch:
diff --git a/docs/data_binding/tree_serializing.md b/docs/data_binding/tree_serializing.md
new file mode 100644
index 00000000..ee3a0ba1
--- /dev/null
+++ b/docs/data_binding/tree_serializing.md
@@ -0,0 +1,57 @@
+# Element Tree Serializer
+
+The element tree serializers will render an object into an element tree, that you can
+use to run XPATH evaluations or XSLT transformations.
+
+There are two implementations based on lxml
+[LxmlTreeSerializer][xsdata.formats.dataclass.serializers.LxmlTreeSerializer] and native
+python [XmlTreeSerializer][xsdata.formats.dataclass.serializers.XmlTreeSerializer].
+
+## xml.etree.ElementTree.Element
+
+```python
+>>> from xml.etree import ElementTree
+>>> from tests.fixtures.books.fixtures import books
+>>> from xsdata.formats.dataclass.serializers import XmlTreeSerializer
+...
+>>> serializer = XmlTreeSerializer()
+>>> result = serializer.render(books)
+...
+>>> result.find(".//title").text
+'The First Book'
+
+```
+
+## lxml.etree.Element
+
+```python
+>>> from lxml import etree
+>>> from tests.fixtures.books.fixtures import books
+>>> from xsdata.formats.dataclass.serializers import LxmlTreeSerializer
+...
+>>> serializer = LxmlTreeSerializer()
+>>> result = serializer.render(books)
+...
+>>> etree.indent(result)
+>>> actual = etree.tostring(result)
+>>> print(actual.decode())
+<ns0:books xmlns:ns0="urn:books">
+  <book>
+    <author>Hightower, Kim</author>
+    <title>The First Book</title>
+    <genre>Fiction</genre>
+    <price>44.95</price>
+    <pub_date>2000-10-01</pub_date>
+    <review>An amazing story of nothing.</review>
+  </book>
+  <book>
+    <author>Nagata, Suanne</author>
+    <title>Becoming Somebody</title>
+    <genre>Biography</genre>
+    <price>33.95</price>
+    <pub_date>2001-01-10</pub_date>
+    <review>A masterpiece of the fine art of gossiping.</review>
+  </book>
+</ns0:books>
+
+```
diff --git a/mkdocs.yml b/mkdocs.yml
index d4ef441b..6e82bf79 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -116,6 +116,7 @@ nav:
   - JSON Parsing: data_binding/json_parsing.md
   - JSON Serializing: data_binding/json_serializing.md
   - Pycode Serializing: data_binding/pycode_serializing.md
+  - Tree Serializing: data_binding/tree_serializing.md
   - Dict Decoding: data_binding/dict_decoding.md
   - Dict Encoding: data_binding/dict_encoding.md
 - FAQ: faq.md
diff --git a/xsdata/formats/dataclass/serializers/__init__.py b/xsdata/formats/dataclass/serializers/__init__.py
index 05d18896..12f2005f 100644
--- a/xsdata/formats/dataclass/serializers/__init__.py
+++ b/xsdata/formats/dataclass/serializers/__init__.py
@@ -1,6 +1,7 @@
 from xsdata.formats.dataclass.serializers.code import PycodeSerializer
 from xsdata.formats.dataclass.serializers.dict import DictEncoder, DictFactory
 from xsdata.formats.dataclass.serializers.json import JsonSerializer
+from xsdata.formats.dataclass.serializers.tree.native import XmlTreeSerializer
 from xsdata.formats.dataclass.serializers.xml import XmlSerializer
 
 __all__ = [
@@ -10,3 +11,10 @@ __all__ = [
     "XmlSerializer",
     "PycodeSerializer",
 ]
+
+try:
+    from xsdata.formats.dataclass.serializers.tree.lxml import LxmlTreeSerializer
+
+    __all__.append("LxmlTreeSerializer")
+except ImportError:  # pragma: no cover
+    pass  # pragma: no cover
diff --git a/xsdata/formats/dataclass/serializers/mixins.py b/xsdata/formats/dataclass/serializers/mixins.py
index 4ab8509e..ca82f2d7 100644
--- a/xsdata/formats/dataclass/serializers/mixins.py
+++ b/xsdata/formats/dataclass/serializers/mixins.py
@@ -1,8 +1,11 @@
 import abc
+from dataclasses import dataclass, field
+from enum import Enum
 from typing import (
     Any,
     Dict,
     Final,
+    Iterable,
     Iterator,
     List,
     Literal,
@@ -16,10 +19,13 @@ from xml.sax.handler import ContentHandler
 
 from typing_extensions import TypeAlias
 
-from xsdata.exceptions import XmlWriterError
+from xsdata.exceptions import SerializerError, XmlWriterError
 from xsdata.formats.converter import converter
+from xsdata.formats.dataclass.context import XmlContext
+from xsdata.formats.dataclass.models.elements import XmlMeta, XmlVar
 from xsdata.formats.dataclass.serializers.config import SerializerConfig
 from xsdata.models.enums import DataType, Namespace, QNames
+from xsdata.utils import collections, namespaces
 from xsdata.utils.constants import EMPTY_MAP
 from xsdata.utils.namespaces import generate_prefix, prefix_exists, split_qname
 
@@ -340,3 +346,565 @@ class XmlWriter(abc.ABC):
             return None
 
         return converter.serialize(data, ns_map=self.ns_map)
+
+
+@dataclass
+class EventGenerator:
+    context: XmlContext = field(default_factory=XmlContext)
+    config: SerializerConfig = field(default_factory=SerializerConfig)
+
+    def generate(self, obj: Any) -> EventIterator:
+        """Convert a user model, or derived element instance to sax events.
+
+        Args:
+            obj: A user model, or derived element instance
+
+        Yields:
+            An iterator of sax events.
+        """
+        qname = xsi_type = None
+        if isinstance(obj, self.context.class_type.derived_element):
+            meta = self.context.build(
+                obj.value.__class__, globalns=self.config.globalns
+            )
+            qname = obj.qname
+            obj = obj.value
+            xsi_type = self.real_xsi_type(qname, meta.target_qname)
+
+        yield from self.convert_dataclass(obj, qname=qname, xsi_type=xsi_type)
+
+    def convert_dataclass(
+        self,
+        obj: Any,
+        namespace: Optional[str] = None,
+        qname: Optional[str] = None,
+        nillable: bool = False,
+        xsi_type: Optional[str] = None,
+    ) -> EventIterator:
+        """Convert a model instance to sax events.
+
+        Optionally override the qualified name and the
+        xsi attributes type and nil.
+
+        Args:
+            obj: A model instance
+            namespace: The field namespace URI
+            qname: Override the field qualified name
+            nillable: Specifies whether the field is nillable
+            xsi_type: Override the field xsi type
+
+        Yields:
+            An iterator of sax events.
+        """
+        meta = self.context.build(
+            obj.__class__,
+            namespace,
+            globalns=self.config.globalns,
+        )
+        qname = qname or meta.qname
+        nillable = nillable or meta.nillable
+        namespace, tag = namespaces.split_qname(qname)
+
+        yield XmlWriterEvent.START, qname
+
+        for key, value in self.next_attribute(
+            obj, meta, nillable, xsi_type, self.config.ignore_default_attributes
+        ):
+            yield XmlWriterEvent.ATTR, key, value
+
+        for var, value in self.next_value(obj, meta):
+            yield from self.convert_value(value, var, namespace)
+
+        yield XmlWriterEvent.END, qname
+
+    def convert_xsi_type(
+        self,
+        value: Any,
+        var: XmlVar,
+        namespace: Optional[str],
+    ) -> EventIterator:
+        """Convert a xsi:type value to sax events.
+
+        The value can be assigned to wildcard, element or compound fields
+
+        Args:
+            value: A model instance
+            var: The field metadata instance
+            namespace: The field namespace URI
+
+        Yields:
+            An iterator of sax events.
+        """
+        if var.is_wildcard:
+            choice = var.find_value_choice(value, True)
+            if choice:
+                yield from self.convert_value(value, choice, namespace)
+            else:
+                yield from self.convert_dataclass(value, namespace)
+        elif var.is_element:
+            xsi_type = self.xsi_type(var, value, namespace)
+            yield from self.convert_dataclass(
+                value,
+                namespace,
+                var.qname,
+                var.nillable,
+                xsi_type,
+            )
+        else:
+            # var elements/compound
+            meta = self.context.fetch(value.__class__, namespace)
+            yield from self.convert_dataclass(value, qname=meta.target_qname)
+
+    def convert_value(
+        self, value: Any, var: XmlVar, namespace: Optional[str]
+    ) -> EventIterator:
+        """Convert any value to sax events according to the var instance.
+
+        The order of the checks is important as more than one condition
+        can be true.
+
+        Args:
+            value: The input value
+            var: The field metadata instance
+            namespace: The class namespace URI
+
+        Yields:
+            An iterator of sax events.
+        """
+        if var.mixed:
+            yield from self.convert_mixed_content(value, var, namespace)
+        elif var.is_text:
+            yield from self.convert_data(value, var)
+        elif var.tokens:
+            yield from self.convert_tokens(value, var, namespace)
+        elif var.is_elements:
+            yield from self.convert_elements(value, var, namespace)
+        elif var.list_element and collections.is_array(value):
+            yield from self.convert_list(value, var, namespace)
+        else:
+            yield from self.convert_any_type(value, var, namespace)
+
+    def convert_list(
+        self,
+        values: Iterable,
+        var: XmlVar,
+        namespace: Optional[str],
+    ) -> EventIterator:
+        """Convert an array of values to sax events.
+
+        Args:
+            values: A list, set, tuple instance
+            var: The field metadata instance
+            namespace: The class namespace
+
+        Yields:
+            An iterator of sax events.
+        """
+        if var.wrapper is not None:
+            yield XmlWriterEvent.START, var.wrapper
+            for value in values:
+                yield from self.convert_value(value, var, namespace)
+            yield XmlWriterEvent.END, var.wrapper
+        else:
+            for value in values:
+                yield from self.convert_value(value, var, namespace)
+
+    def convert_tokens(
+        self, value: Any, var: XmlVar, namespace: Optional[str]
+    ) -> EventIterator:
+        """Convert an array of token values to sax events.
+
+        Args:
+            value: A list, set, tuple instance
+            var: The field metadata instance
+            namespace: The class namespace
+
+        Yields:
+            An iterator of sax events.
+        """
+        if value or var.nillable or var.required:
+            if value and collections.is_array(value[0]):
+                for val in value:
+                    yield from self.convert_element(val, var, namespace)
+            else:
+                yield from self.convert_element(value, var, namespace)
+
+    def convert_mixed_content(
+        self,
+        values: List,
+        var: XmlVar,
+        namespace: Optional[str],
+    ) -> EventIterator:
+        """Convert mixed content values to sax events.
+
+        Args:
+            values: A list instance of mixed type values
+            var: The field metadata instance
+            namespace: The class namespace
+
+        Yields:
+            An iterator of sax events.
+        """
+        for value in values:
+            yield from self.convert_any_type(value, var, namespace)
+
+    def convert_any_type(
+        self, value: Any, var: XmlVar, namespace: Optional[str]
+    ) -> EventIterator:
+        """Convert a value assigned to a xs:anyType field to sax events.
+
+        Args:
+            value: A list instance of mixed type values
+            var: The field metadata instance
+            namespace: The class namespace
+
+        Yields:
+            An iterator of sax events.
+        """
+        if isinstance(value, self.context.class_type.any_element):
+            yield from self.convert_any_element(value, var, namespace)
+        elif isinstance(value, self.context.class_type.derived_element):
+            yield from self.convert_derived_element(value, namespace)
+        elif self.context.class_type.is_model(value):
+            yield from self.convert_xsi_type(value, var, namespace)
+        elif var.is_element:
+            yield from self.convert_element(value, var, namespace)
+        else:
+            yield from self.convert_data(value, var)
+
+    def convert_derived_element(
+        self, value: Any, namespace: Optional[str]
+    ) -> EventIterator:
+        """Convert a derived element instance to sax events.
+
+        Args:
+            value: A list instance of mixed type values
+            namespace: The class namespace
+
+        Yields:
+            An iterator of sax events.
+        """
+        if self.context.class_type.is_model(value.value):
+            meta = self.context.fetch(value.value.__class__)
+            qname = value.qname
+            xsi_type = self.real_xsi_type(qname, meta.target_qname)
+
+            yield from self.convert_dataclass(
+                value.value, namespace, qname=qname, xsi_type=xsi_type
+            )
+        else:
+            datatype = DataType.from_value(value.value)
+
+            yield XmlWriterEvent.START, value.qname
+            yield XmlWriterEvent.ATTR, QNames.XSI_TYPE, QName(str(datatype))
+            yield XmlWriterEvent.DATA, value.value
+            yield XmlWriterEvent.END, value.qname
+
+    def convert_any_element(
+        self, value: Any, var: XmlVar, namespace: Optional[str]
+    ) -> EventIterator:
+        """Convert a generic any element instance to sax events.
+
+        Args:
+            value: A list instance of mixed type values
+            var: The field metadata instance
+            namespace: The class namespace
+
+        Yields:
+            An iterator of sax events.
+        """
+        if value.qname:
+            namespace, tag = namespaces.split_qname(value.qname)
+            yield XmlWriterEvent.START, value.qname
+
+        for key, val in value.attributes.items():
+            yield XmlWriterEvent.ATTR, key, val
+
+        yield XmlWriterEvent.DATA, value.text
+
+        for child in value.children:
+            yield from self.convert_any_type(child, var, namespace)
+
+        if value.qname:
+            yield XmlWriterEvent.END, value.qname
+
+        if value.tail:
+            yield XmlWriterEvent.DATA, value.tail
+
+    def xsi_type(
+        self, var: XmlVar, value: Any, namespace: Optional[str]
+    ) -> Optional[str]:
+        """Return the xsi:type for the given value and field metadata instance.
+
+        If the value type is either a child or parent for one of the var types,
+        we need to declare it as n xsi:type.
+
+        Args:
+            value: A list instance of mixed type values
+            var: The field metadata instance
+            namespace: The class namespace
+
+        Raises:
+            SerializerError: If the value type is completely unrelated to
+                the field types.
+        """
+        if not value or value.__class__ in var.types:
+            return None
+
+        clazz = var.clazz
+        if clazz is None or self.context.is_derived(value, clazz):
+            meta = self.context.fetch(value.__class__, namespace)
+            return self.real_xsi_type(var.qname, meta.target_qname)
+
+        raise SerializerError(
+            f"{value.__class__.__name__} is not derived from {clazz.__name__}"
+        )
+
+    def convert_elements(
+        self, value: Any, var: XmlVar, namespace: Optional[str]
+    ) -> EventIterator:
+        """Convert the value assigned to a compound field to sax events.
+
+        Args:
+            value: A list instance of mixed type values
+            var: The field metadata instance
+            namespace: The class namespace
+
+        Yields:
+            An iterator of sax events.
+        """
+        if collections.is_array(value):
+            for val in value:
+                yield from self.convert_choice(val, var, namespace)
+        else:
+            yield from self.convert_choice(value, var, namespace)
+
+    def convert_choice(
+        self, value: Any, var: XmlVar, namespace: Optional[str]
+    ) -> EventIterator:
+        """Convert a single value assigned to a compound field to sax events.
+
+        Args:
+            value: A list instance of mixed type values
+            var: The field metadata instance
+            namespace: The class namespace
+
+        Yields:
+            An iterator of sax events.
+
+        Raises:
+            SerializerError: If the value doesn't match any choice field.
+        """
+        if isinstance(value, self.context.class_type.derived_element):
+            choice = var.find_choice(value.qname)
+            value = value.value
+
+            if self.context.class_type.is_model(value):
+                func = self.convert_xsi_type
+            else:
+                func = self.convert_element
+
+        elif isinstance(value, self.context.class_type.any_element) and value.qname:
+            choice = var.find_choice(value.qname)
+            func = self.convert_any_type
+        else:
+            check_subclass = self.context.class_type.is_model(value)
+            choice = var.find_value_choice(value, check_subclass)
+            func = self.convert_value
+
+            if not choice and check_subclass:
+                func = self.convert_xsi_type
+                choice = var
+
+        if not choice:
+            raise SerializerError(
+                f"XmlElements undefined choice: `{var.name}` for `{type(value)}`"
+            )
+
+        yield from func(value, choice, namespace)
+
+    def convert_element(
+        self,
+        value: Any,
+        var: XmlVar,
+        namespace: Optional[str],
+    ) -> EventIterator:
+        """Convert a value assigned to an element field to sax events.
+
+        Args:
+            value: A list instance of mixed type values
+            var: The field metadata instance
+            namespace: The class namespace (unused)
+
+        Yields:
+            An iterator of sax events.
+        """
+        yield XmlWriterEvent.START, var.qname
+
+        if var.nillable:
+            yield XmlWriterEvent.ATTR, QNames.XSI_NIL, "true"
+
+        if value is not None and value != "" and var.any_type:
+            datatype = DataType.from_value(value)
+            if datatype != DataType.STRING:
+                yield XmlWriterEvent.ATTR, QNames.XSI_TYPE, QName(str(datatype))
+
+        yield XmlWriterEvent.DATA, self.encode_primitive(value, var)
+        yield XmlWriterEvent.END, var.qname
+
+    @classmethod
+    def convert_data(cls, value: Any, var: XmlVar) -> EventIterator:
+        """Convert a value assigned to a text field to sax events.
+
+        Args:
+            value: A list instance of mixed type values
+            var: The field metadata instance
+
+        Yields:
+            An iterator of sax events.
+        """
+        yield XmlWriterEvent.DATA, cls.encode_primitive(value, var)
+
+    @classmethod
+    def next_value(cls, obj: Any, meta: XmlMeta) -> Iterator[Tuple[XmlVar, Any]]:
+        """Produce the next non attribute value of a model instance to convert.
+
+        The generator will produce the values in the order the fields
+        are defined in the model or by their sequence number.
+
+        Sequential fields need to be rendered together in parallel order
+        eg: <a1/><a2/><a1/><a/2></a1>
+
+        Args:
+            obj: The input model instance
+            meta: The model metadata instance
+
+        Yields:
+            An iterator of field metadata instance and value tuples.
+        """
+        index = 0
+        attrs = meta.get_element_vars()
+        stop = len(attrs)
+        while index < stop:
+            var = attrs[index]
+
+            if var.sequence is None:
+                value = getattr(obj, var.name)
+                if value is not None or var.nillable:
+                    yield var, value
+                index += 1
+                continue
+
+            indices = range(index, stop)
+            end = next(
+                i for i in indices[::-1] if attrs[i].sequence == var.sequence
+            )  # pragma: no cover
+            sequence = attrs[index : end + 1]
+            index = end + 1
+            j = 0
+
+            rolling = True
+            while rolling:
+                rolling = False
+                for var in sequence:
+                    values = getattr(obj, var.name)
+                    if collections.is_array(values):
+                        if j < len(values):
+                            rolling = True
+                            value = values[j]
+                            if value is not None or var.nillable:
+                                yield var, value
+                    elif j == 0:
+                        rolling = True
+                        if values is not None or var.nillable:
+                            yield var, values
+
+                j += 1
+
+    @classmethod
+    def next_attribute(
+        cls,
+        obj: Any,
+        meta: XmlMeta,
+        nillable: bool,
+        xsi_type: Optional[str],
+        ignore_optionals: bool,
+    ) -> Iterator[Tuple[str, Any]]:
+        """Produce the next attribute value to convert.
+
+        Args:
+            obj: The input model instance
+            meta: The model metadata instance
+            nillable: Specifies if the current element supports nillable content
+            xsi_type: The real xsi:type of the object
+            ignore_optionals: Specifies if optional attributes with default
+                values should be ignored.
+
+        Yields:
+            An iterator of attribute name-value pairs.
+        """
+        for var in meta.get_attribute_vars():
+            if var.is_attribute:
+                value = getattr(obj, var.name)
+                if (
+                    value is None
+                    or (collections.is_array(value) and not value)
+                    or (ignore_optionals and var.is_optional(value))
+                ):
+                    continue
+
+                yield var.qname, cls.encode_primitive(value, var)
+            else:
+                yield from getattr(obj, var.name, EMPTY_MAP).items()
+
+        if xsi_type:
+            yield QNames.XSI_TYPE, QName(xsi_type)
+
+        if nillable:
+            yield QNames.XSI_NIL, "true"
+
+    @classmethod
+    def encode_primitive(cls, value: Any, var: XmlVar) -> Any:
+        """Encode a value for xml serialization.
+
+        Converts values to strings. QName instances is an exception,
+        those values need to wait until the XmlWriter assigns prefixes
+        to namespaces per element node. Enums and Tokens may contain
+        QName(s) so they also get a special treatment.
+
+        We can't do all the conversions in the writer because we would
+        need to carry the xml vars inside the writer. Instead of that we
+        do the easy encoding here and leave the qualified names for
+        later.
+
+        Args:
+            value: The simple type vale to encode
+            var: The field metadata instance
+
+        Returns:
+            The encoded value.
+        """
+        if isinstance(value, (str, QName)) or var is None:
+            return value
+
+        if collections.is_array(value):
+            return [cls.encode_primitive(v, var) for v in value]
+
+        if isinstance(value, Enum):
+            return cls.encode_primitive(value.value, var)
+
+        return converter.serialize(value, format=var.format)
+
+    @classmethod
+    def real_xsi_type(cls, qname: str, target_qname: Optional[str]) -> Optional[str]:
+        """Compare the qname with the target qname and return the real xsi:type.
+
+        Args:
+            qname: The field type qualified name
+            target_qname: The value type qualified name
+
+        Returns:
+            None if the qname and target qname match, otherwise
+            return the target qname.
+        """
+        return target_qname if target_qname != qname else None
diff --git a/xsdata/formats/dataclass/serializers/tree/__init__.py b/xsdata/formats/dataclass/serializers/tree/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/xsdata/formats/dataclass/serializers/tree/lxml.py b/xsdata/formats/dataclass/serializers/tree/lxml.py
new file mode 100644
index 00000000..ea796caf
--- /dev/null
+++ b/xsdata/formats/dataclass/serializers/tree/lxml.py
@@ -0,0 +1,22 @@
+from dataclasses import dataclass
+
+from lxml.etree import Element, TreeBuilder
+
+from xsdata.formats.bindings import T
+from xsdata.formats.dataclass.serializers.tree.mixins import TreeSerializer
+
+
+@dataclass
+class LxmlTreeSerializer(TreeSerializer):
+    def render(self, obj: T) -> Element:
+        """Serialize the input model instance to element tree.
+
+        Args:
+            obj: The input model instance to serialize
+
+        Returns:
+            The generated element tree instance.
+        """
+        builder = TreeBuilder()
+        self.build(obj, builder)
+        return builder.close()
diff --git a/xsdata/formats/dataclass/serializers/tree/mixins.py b/xsdata/formats/dataclass/serializers/tree/mixins.py
new file mode 100644
index 00000000..10c7bc6f
--- /dev/null
+++ b/xsdata/formats/dataclass/serializers/tree/mixins.py
@@ -0,0 +1,59 @@
+import abc
+from dataclasses import dataclass
+from typing import Any, Dict, Protocol
+
+from xsdata.exceptions import XmlHandlerError
+from xsdata.formats.bindings import T
+from xsdata.formats.dataclass.serializers.mixins import EventGenerator, XmlWriterEvent
+from xsdata.models.enums import EventType
+
+
+class TreeBuilder(Protocol):
+    def start(self, tag: str, attrs: Dict[str, Any]): ...  # pragma: no cover
+
+    def end(self, tag: str): ...  # pragma: no cover
+
+    def data(self, data: str): ...  # pragma: no cover
+
+
+@dataclass
+class TreeSerializer(EventGenerator):
+    @abc.abstractmethod
+    def render(self, obj: T) -> Any:
+        """Serialize the input model instance to element tree.
+
+        Args:
+            obj: The input model instance to serialize
+
+        Returns:
+            The generated element tree instance.
+        """
+
+    def build(self, obj: T, builder: TreeBuilder):
+        """Feed the builder with events from input model instance.
+
+        Args:
+            obj: The input model instance to serialize
+            builder: The tree builder instance
+        """
+        pending_tag = None
+        pending_attrs: Dict[str, Any] = {}
+        for event, *element in self.generate(obj):
+            if pending_tag is not None:
+                builder.start(pending_tag, pending_attrs)
+                pending_tag = None
+                pending_attrs = {}
+
+            if event == XmlWriterEvent.START:
+                pending_tag = element[0]
+                pending_attrs = {}
+            elif event == XmlWriterEvent.ATTR:
+                key, value = element
+                pending_attrs[key] = value
+
+            elif event == EventType.END:
+                builder.end(*element)
+            elif event == XmlWriterEvent.DATA:
+                builder.data(*element)
+            else:
+                raise XmlHandlerError(f"Unhandled event: `{event}`.")
diff --git a/xsdata/formats/dataclass/serializers/tree/native.py b/xsdata/formats/dataclass/serializers/tree/native.py
new file mode 100644
index 00000000..c64eed52
--- /dev/null
+++ b/xsdata/formats/dataclass/serializers/tree/native.py
@@ -0,0 +1,21 @@
+from dataclasses import dataclass
+from xml.etree.ElementTree import Element, TreeBuilder
+
+from xsdata.formats.bindings import T
+from xsdata.formats.dataclass.serializers.tree.mixins import TreeSerializer
+
+
+@dataclass
+class XmlTreeSerializer(TreeSerializer):
+    def render(self, obj: T) -> Element:
+        """Serialize the input model instance to element tree.
+
+        Args:
+            obj: The input model instance to serialize
+
+        Returns:
+            The generated element tree instance.
+        """
+        builder = TreeBuilder()
+        self.build(obj, builder)
+        return builder.close()
diff --git a/xsdata/formats/dataclass/serializers/xml.py b/xsdata/formats/dataclass/serializers/xml.py
index 2425b4dc..df0a23c2 100644
--- a/xsdata/formats/dataclass/serializers/xml.py
+++ b/xsdata/formats/dataclass/serializers/xml.py
@@ -21,6 +21,7 @@ from xsdata.formats.dataclass.context import XmlContext
 from xsdata.formats.dataclass.models.elements import XmlMeta, XmlVar
 from xsdata.formats.dataclass.serializers.config import SerializerConfig
 from xsdata.formats.dataclass.serializers.mixins import (
+    EventGenerator,
     EventIterator,
     XmlWriter,
     XmlWriterEvent,
@@ -32,7 +33,7 @@ from xsdata.utils.constants import EMPTY_MAP
 
 
 @dataclass
-class XmlSerializer(AbstractSerializer):
+class XmlSerializer(EventGenerator, AbstractSerializer):
     """Xml serializer for data classes.
 
     Args:
@@ -41,8 +42,6 @@ class XmlSerializer(AbstractSerializer):
         writer: The xml writer class
     """
 
-    config: SerializerConfig = field(default_factory=SerializerConfig)
-    context: XmlContext = field(default_factory=XmlContext)
     writer: Type[XmlWriter] = field(default=default_writer())
 
     def render(self, obj: Any, ns_map: Optional[Dict] = None) -> str:
@@ -67,566 +66,10 @@ class XmlSerializer(AbstractSerializer):
             obj: The input model instance to serialize
             ns_map: A user defined namespace prefix-URI map
         """
-        events = self.write_object(obj)
+        events = self.generate(obj)
         handler = self.writer(
             config=self.config,
             output=out,
             ns_map=namespaces.clean_prefixes(ns_map) if ns_map else {},
         )
         handler.write(events)
-
-    def write_object(self, obj: Any) -> EventIterator:
-        """Convert a user model, or derived element instance to sax events.
-
-        Args:
-            obj: A user model, or derived element instance
-
-        Yields:
-            An iterator of sax events.
-        """
-        qname = xsi_type = None
-        if isinstance(obj, self.context.class_type.derived_element):
-            meta = self.context.build(
-                obj.value.__class__, globalns=self.config.globalns
-            )
-            qname = obj.qname
-            obj = obj.value
-            xsi_type = self.real_xsi_type(qname, meta.target_qname)
-
-        yield from self.write_dataclass(obj, qname=qname, xsi_type=xsi_type)
-
-    def write_dataclass(
-        self,
-        obj: Any,
-        namespace: Optional[str] = None,
-        qname: Optional[str] = None,
-        nillable: bool = False,
-        xsi_type: Optional[str] = None,
-    ) -> EventIterator:
-        """Convert a model instance to sax events.
-
-        Optionally override the qualified name and the
-        xsi attributes type and nil.
-
-        Args:
-            obj: A model instance
-            namespace: The field namespace URI
-            qname: Override the field qualified name
-            nillable: Specifies whether the field is nillable
-            xsi_type: Override the field xsi type
-
-        Yields:
-            An iterator of sax events.
-        """
-        meta = self.context.build(
-            obj.__class__,
-            namespace,
-            globalns=self.config.globalns,
-        )
-        qname = qname or meta.qname
-        nillable = nillable or meta.nillable
-        namespace, tag = namespaces.split_qname(qname)
-
-        yield XmlWriterEvent.START, qname
-
-        for key, value in self.next_attribute(
-            obj, meta, nillable, xsi_type, self.config.ignore_default_attributes
-        ):
-            yield XmlWriterEvent.ATTR, key, value
-
-        for var, value in self.next_value(obj, meta):
-            yield from self.write_value(value, var, namespace)
-
-        yield XmlWriterEvent.END, qname
-
-    def write_xsi_type(
-        self,
-        value: Any,
-        var: XmlVar,
-        namespace: Optional[str],
-    ) -> EventIterator:
-        """Convert a xsi:type value to sax events.
-
-        The value can be assigned to wildcard, element or compound fields
-
-        Args:
-            value: A model instance
-            var: The field metadata instance
-            namespace: The field namespace URI
-
-        Yields:
-            An iterator of sax events.
-        """
-        if var.is_wildcard:
-            choice = var.find_value_choice(value, True)
-            if choice:
-                yield from self.write_value(value, choice, namespace)
-            else:
-                yield from self.write_dataclass(value, namespace)
-        elif var.is_element:
-            xsi_type = self.xsi_type(var, value, namespace)
-            yield from self.write_dataclass(
-                value,
-                namespace,
-                var.qname,
-                var.nillable,
-                xsi_type,
-            )
-        else:
-            # var elements/compound
-            meta = self.context.fetch(value.__class__, namespace)
-            yield from self.write_dataclass(value, qname=meta.target_qname)
-
-    def write_value(
-        self, value: Any, var: XmlVar, namespace: Optional[str]
-    ) -> EventIterator:
-        """Convert any value to sax events according to the var instance.
-
-        The order of the checks is important as more than one condition
-        can be true.
-
-        Args:
-            value: The input value
-            var: The field metadata instance
-            namespace: The class namespace URI
-
-        Yields:
-            An iterator of sax events.
-        """
-        if var.mixed:
-            yield from self.write_mixed_content(value, var, namespace)
-        elif var.is_text:
-            yield from self.write_data(value, var)
-        elif var.tokens:
-            yield from self.write_tokens(value, var, namespace)
-        elif var.is_elements:
-            yield from self.write_elements(value, var, namespace)
-        elif var.list_element and collections.is_array(value):
-            yield from self.write_list(value, var, namespace)
-        else:
-            yield from self.write_any_type(value, var, namespace)
-
-    def write_list(
-        self,
-        values: Iterable,
-        var: XmlVar,
-        namespace: Optional[str],
-    ) -> EventIterator:
-        """Convert an array of values to sax events.
-
-        Args:
-            values: A list, set, tuple instance
-            var: The field metadata instance
-            namespace: The class namespace
-
-        Yields:
-            An iterator of sax events.
-        """
-        if var.wrapper is not None:
-            yield XmlWriterEvent.START, var.wrapper
-            for value in values:
-                yield from self.write_value(value, var, namespace)
-            yield XmlWriterEvent.END, var.wrapper
-        else:
-            for value in values:
-                yield from self.write_value(value, var, namespace)
-
-    def write_tokens(
-        self, value: Any, var: XmlVar, namespace: Optional[str]
-    ) -> EventIterator:
-        """Convert an array of token values to sax events.
-
-        Args:
-            value: A list, set, tuple instance
-            var: The field metadata instance
-            namespace: The class namespace
-
-        Yields:
-            An iterator of sax events.
-        """
-        if value or var.nillable or var.required:
-            if value and collections.is_array(value[0]):
-                for val in value:
-                    yield from self.write_element(val, var, namespace)
-            else:
-                yield from self.write_element(value, var, namespace)
-
-    def write_mixed_content(
-        self,
-        values: List,
-        var: XmlVar,
-        namespace: Optional[str],
-    ) -> EventIterator:
-        """Convert mixed content values to sax events.
-
-        Args:
-            values: A list instance of mixed type values
-            var: The field metadata instance
-            namespace: The class namespace
-
-        Yields:
-            An iterator of sax events.
-        """
-        for value in values:
-            yield from self.write_any_type(value, var, namespace)
-
-    def write_any_type(
-        self, value: Any, var: XmlVar, namespace: Optional[str]
-    ) -> EventIterator:
-        """Convert a value assigned to a xs:anyType field to sax events.
-
-        Args:
-            value: A list instance of mixed type values
-            var: The field metadata instance
-            namespace: The class namespace
-
-        Yields:
-            An iterator of sax events.
-        """
-        if isinstance(value, self.context.class_type.any_element):
-            yield from self.write_any_element(value, var, namespace)
-        elif isinstance(value, self.context.class_type.derived_element):
-            yield from self.write_derived_element(value, namespace)
-        elif self.context.class_type.is_model(value):
-            yield from self.write_xsi_type(value, var, namespace)
-        elif var.is_element:
-            yield from self.write_element(value, var, namespace)
-        else:
-            yield from self.write_data(value, var)
-
-    def write_derived_element(
-        self, value: Any, namespace: Optional[str]
-    ) -> EventIterator:
-        """Convert a derived element instance to sax events.
-
-        Args:
-            value: A list instance of mixed type values
-            namespace: The class namespace
-
-        Yields:
-            An iterator of sax events.
-        """
-        if self.context.class_type.is_model(value.value):
-            meta = self.context.fetch(value.value.__class__)
-            qname = value.qname
-            xsi_type = self.real_xsi_type(qname, meta.target_qname)
-
-            yield from self.write_dataclass(
-                value.value, namespace, qname=qname, xsi_type=xsi_type
-            )
-        else:
-            datatype = DataType.from_value(value.value)
-
-            yield XmlWriterEvent.START, value.qname
-            yield XmlWriterEvent.ATTR, QNames.XSI_TYPE, QName(str(datatype))
-            yield XmlWriterEvent.DATA, value.value
-            yield XmlWriterEvent.END, value.qname
-
-    def write_any_element(
-        self, value: Any, var: XmlVar, namespace: Optional[str]
-    ) -> EventIterator:
-        """Convert a generic any element instance to sax events.
-
-        Args:
-            value: A list instance of mixed type values
-            var: The field metadata instance
-            namespace: The class namespace
-
-        Yields:
-            An iterator of sax events.
-        """
-        if value.qname:
-            namespace, tag = namespaces.split_qname(value.qname)
-            yield XmlWriterEvent.START, value.qname
-
-        for key, val in value.attributes.items():
-            yield XmlWriterEvent.ATTR, key, val
-
-        yield XmlWriterEvent.DATA, value.text
-
-        for child in value.children:
-            yield from self.write_any_type(child, var, namespace)
-
-        if value.qname:
-            yield XmlWriterEvent.END, value.qname
-
-        if value.tail:
-            yield XmlWriterEvent.DATA, value.tail
-
-    def xsi_type(
-        self, var: XmlVar, value: Any, namespace: Optional[str]
-    ) -> Optional[str]:
-        """Return the xsi:type for the given value and field metadata instance.
-
-        If the value type is either a child or parent for one of the var types,
-        we need to declare it as n xsi:type.
-
-        Args:
-            value: A list instance of mixed type values
-            var: The field metadata instance
-            namespace: The class namespace
-
-        Raises:
-            SerializerError: If the value type is completely unrelated to
-                the field types.
-        """
-        if not value or value.__class__ in var.types:
-            return None
-
-        clazz = var.clazz
-        if clazz is None or self.context.is_derived(value, clazz):
-            meta = self.context.fetch(value.__class__, namespace)
-            return self.real_xsi_type(var.qname, meta.target_qname)
-
-        raise SerializerError(
-            f"{value.__class__.__name__} is not derived from {clazz.__name__}"
-        )
-
-    def write_elements(
-        self, value: Any, var: XmlVar, namespace: Optional[str]
-    ) -> EventIterator:
-        """Convert the value assigned to a compound field to sax events.
-
-        Args:
-            value: A list instance of mixed type values
-            var: The field metadata instance
-            namespace: The class namespace
-
-        Yields:
-            An iterator of sax events.
-        """
-        if collections.is_array(value):
-            for val in value:
-                yield from self.write_choice(val, var, namespace)
-        else:
-            yield from self.write_choice(value, var, namespace)
-
-    def write_choice(
-        self, value: Any, var: XmlVar, namespace: Optional[str]
-    ) -> EventIterator:
-        """Convert a single value assigned to a compound field to sax events.
-
-        Args:
-            value: A list instance of mixed type values
-            var: The field metadata instance
-            namespace: The class namespace
-
-        Yields:
-            An iterator of sax events.
-
-        Raises:
-            SerializerError: If the value doesn't match any choice field.
-        """
-        if isinstance(value, self.context.class_type.derived_element):
-            choice = var.find_choice(value.qname)
-            value = value.value
-
-            if self.context.class_type.is_model(value):
-                func = self.write_xsi_type
-            else:
-                func = self.write_element
-
-        elif isinstance(value, self.context.class_type.any_element) and value.qname:
-            choice = var.find_choice(value.qname)
-            func = self.write_any_type
-        else:
-            check_subclass = self.context.class_type.is_model(value)
-            choice = var.find_value_choice(value, check_subclass)
-            func = self.write_value
-
-            if not choice and check_subclass:
-                func = self.write_xsi_type
-                choice = var
-
-        if not choice:
-            raise SerializerError(
-                f"XmlElements undefined choice: `{var.name}` for `{type(value)}`"
-            )
-
-        yield from func(value, choice, namespace)
-
-    def write_element(
-        self,
-        value: Any,
-        var: XmlVar,
-        namespace: Optional[str],
-    ) -> EventIterator:
-        """Convert a value assigned to an element field to sax events.
-
-        Args:
-            value: A list instance of mixed type values
-            var: The field metadata instance
-            namespace: The class namespace (unused)
-
-        Yields:
-            An iterator of sax events.
-        """
-        yield XmlWriterEvent.START, var.qname
-
-        if var.nillable:
-            yield XmlWriterEvent.ATTR, QNames.XSI_NIL, "true"
-
-        if value is not None and value != "" and var.any_type:
-            datatype = DataType.from_value(value)
-            if datatype != DataType.STRING:
-                yield XmlWriterEvent.ATTR, QNames.XSI_TYPE, QName(str(datatype))
-
-        yield XmlWriterEvent.DATA, self.encode(value, var)
-        yield XmlWriterEvent.END, var.qname
-
-    @classmethod
-    def write_data(cls, value: Any, var: XmlVar) -> EventIterator:
-        """Convert a value assigned to a text field to sax events.
-
-        Args:
-            value: A list instance of mixed type values
-            var: The field metadata instance
-
-        Yields:
-            An iterator of sax events.
-        """
-        yield XmlWriterEvent.DATA, cls.encode(value, var)
-
-    @classmethod
-    def next_value(cls, obj: Any, meta: XmlMeta) -> Iterator[Tuple[XmlVar, Any]]:
-        """Produce the next non attribute value of a model instance to convert.
-
-        The generator will produce the values in the order the fields
-        are defined in the model or by their sequence number.
-
-        Sequential fields need to be rendered together in parallel order
-        eg: <a1/><a2/><a1/><a/2></a1>
-
-        Args:
-            obj: The input model instance
-            meta: The model metadata instance
-
-        Yields:
-            An iterator of field metadata instance and value tuples.
-        """
-        index = 0
-        attrs = meta.get_element_vars()
-        stop = len(attrs)
-        while index < stop:
-            var = attrs[index]
-
-            if var.sequence is None:
-                value = getattr(obj, var.name)
-                if value is not None or var.nillable:
-                    yield var, value
-                index += 1
-                continue
-
-            indices = range(index, stop)
-            end = next(
-                i for i in indices[::-1] if attrs[i].sequence == var.sequence
-            )  # pragma: no cover
-            sequence = attrs[index : end + 1]
-            index = end + 1
-            j = 0
-
-            rolling = True
-            while rolling:
-                rolling = False
-                for var in sequence:
-                    values = getattr(obj, var.name)
-                    if collections.is_array(values):
-                        if j < len(values):
-                            rolling = True
-                            value = values[j]
-                            if value is not None or var.nillable:
-                                yield var, value
-                    elif j == 0:
-                        rolling = True
-                        if values is not None or var.nillable:
-                            yield var, values
-
-                j += 1
-
-    @classmethod
-    def next_attribute(
-        cls,
-        obj: Any,
-        meta: XmlMeta,
-        nillable: bool,
-        xsi_type: Optional[str],
-        ignore_optionals: bool,
-    ) -> Iterator[Tuple[str, Any]]:
-        """Produce the next attribute value to convert.
-
-        Args:
-            obj: The input model instance
-            meta: The model metadata instance
-            nillable: Specifies if the current element supports nillable content
-            xsi_type: The real xsi:type of the object
-            ignore_optionals: Specifies if optional attributes with default
-                values should be ignored.
-
-        Yields:
-            An iterator of attribute name-value pairs.
-        """
-        for var in meta.get_attribute_vars():
-            if var.is_attribute:
-                value = getattr(obj, var.name)
-                if (
-                    value is None
-                    or (collections.is_array(value) and not value)
-                    or (ignore_optionals and var.is_optional(value))
-                ):
-                    continue
-
-                yield var.qname, cls.encode(value, var)
-            else:
-                yield from getattr(obj, var.name, EMPTY_MAP).items()
-
-        if xsi_type:
-            yield QNames.XSI_TYPE, QName(xsi_type)
-
-        if nillable:
-            yield QNames.XSI_NIL, "true"
-
-    @classmethod
-    def encode(cls, value: Any, var: XmlVar) -> Any:
-        """Encode a value for xml serialization.
-
-        Converts values to strings. QName instances is an exception,
-        those values need to wait until the XmlWriter assigns prefixes
-        to namespaces per element node. Enums and Tokens may contain
-        QName(s) so they also get a special treatment.
-
-        We can't do all the conversions in the writer because we would
-        need to carry the xml vars inside the writer. Instead of that we
-        do the easy encoding here and leave the qualified names for
-        later.
-
-        Args:
-            value: The simple type vale to encode
-            var: The field metadata instance
-
-        Returns:
-            The encoded value.
-        """
-        if isinstance(value, (str, QName)) or var is None:
-            return value
-
-        if collections.is_array(value):
-            return [cls.encode(v, var) for v in value]
-
-        if isinstance(value, Enum):
-            return cls.encode(value.value, var)
-
-        return converter.serialize(value, format=var.format)
-
-    @classmethod
-    def real_xsi_type(cls, qname: str, target_qname: Optional[str]) -> Optional[str]:
-        """Compare the qname with the target qname and return the real xsi:type.
-
-        Args:
-            qname: The field type qualified name
-            target_qname: The value type qualified name
-
-        Returns:
-            None if the qname and target qname match, otherwise
-            return the target qname.
-        """
-        return target_qname if target_qname != qname else None


Test patch:
diff --git a/tests/formats/dataclass/serializers/test_mixins.py b/tests/formats/dataclass/serializers/test_mixins.py
index c4e00730..424948ce 100644
--- a/tests/formats/dataclass/serializers/test_mixins.py
+++ b/tests/formats/dataclass/serializers/test_mixins.py
@@ -1,12 +1,25 @@
+from dataclasses import dataclass, field, make_dataclass
 from io import StringIO
+from typing import Generator, List
 from unittest import TestCase
+from xml.etree.ElementTree import QName
 from xml.sax import ContentHandler
 from xml.sax.saxutils import XMLGenerator
 
-from xsdata.exceptions import XmlWriterError
+from tests.fixtures.books import BookForm
+from tests.fixtures.datatypes import Telephone
+from tests.fixtures.models import Paragraph, SequentialType, Span, TypeA
+from xsdata.exceptions import SerializerError, XmlContextError, XmlWriterError
+from xsdata.formats.dataclass.models.elements import XmlType
+from xsdata.formats.dataclass.models.generics import AnyElement, DerivedElement
+from xsdata.formats.dataclass.serializers import XmlSerializer
 from xsdata.formats.dataclass.serializers.config import SerializerConfig
-from xsdata.formats.dataclass.serializers.mixins import XmlWriter, XmlWriterEvent
+from xsdata.formats.dataclass.serializers.mixins import (
+    EventGenerator,
+    XmlWriter,
+)
 from xsdata.models.enums import DataType, QNames
+from xsdata.utils.testing import XmlVarFactory
 
 
 class XmlWriterImpl(XmlWriter):
@@ -29,13 +42,13 @@ class XmlWriterTests(TestCase):
     def test_write(self):
         events = iter(
             [
-                (XmlWriterEvent.START, "{http://www.w3.org/1999/xhtml}p"),
-                (XmlWriterEvent.ATTR, "class", "section"),
-                (XmlWriterEvent.DATA, "total:"),
-                (XmlWriterEvent.DATA, 105.22),
-                (XmlWriterEvent.START, "{http://www.w3.org/1999/xhtml}br"),
-                (XmlWriterEvent.END, "{http://www.w3.org/1999/xhtml}br"),
-                (XmlWriterEvent.END, "{http://www.w3.org/1999/xhtml}p"),
+                ("start", "{http://www.w3.org/1999/xhtml}p"),
+                ("attr", "class", "section"),
+                ("data", "total:"),
+                ("data", 105.22),
+                ("start", "{http://www.w3.org/1999/xhtml}br"),
+                ("end", "{http://www.w3.org/1999/xhtml}br"),
+                ("end", "{http://www.w3.org/1999/xhtml}p"),
             ]
         )
 
@@ -53,12 +66,12 @@ class XmlWriterTests(TestCase):
             lines[1],
         )
 
-    def test_write_with_schema_location(self):
+    def test_convert_with_schema_location(self):
         self.writer.config.schema_location = "foo bar"
         events = iter(
             [
-                (XmlWriterEvent.START, "root"),
-                (XmlWriterEvent.END, "root"),
+                ("start", "root"),
+                ("end", "root"),
             ]
         )
 
@@ -73,12 +86,12 @@ class XmlWriterTests(TestCase):
             lines[1],
         )
 
-    def test_write_with_no_namespace_schema_location(self):
+    def test_convert_with_no_namespace_schema_location(self):
         self.writer.config.no_namespace_schema_location = "foo.xsd"
         events = iter(
             [
-                (XmlWriterEvent.START, "root"),
-                (XmlWriterEvent.END, "root"),
+                ("start", "root"),
+                ("end", "root"),
             ]
         )
 
@@ -93,7 +106,7 @@ class XmlWriterTests(TestCase):
             lines[1],
         )
 
-    def test_write_with_unhandled_event_raises_exception(self):
+    def test_convert_with_unhandled_event_raises_exception(self):
         events = iter([("reverse", "p")])
 
         with self.assertRaises(XmlWriterError) as cm:
@@ -101,34 +114,34 @@ class XmlWriterTests(TestCase):
 
         self.assertEqual("Unhandled event: `reverse`", str(cm.exception))
 
-    def test_write_removes_xsi_nil_if_necessary(self):
+    def test_convert_removes_xsi_nil_if_necessary(self):
         events = iter(
             [
-                (XmlWriterEvent.START, "root"),
-                (XmlWriterEvent.START, "a"),
-                (XmlWriterEvent.ATTR, QNames.XSI_NIL, "true"),
-                (XmlWriterEvent.END, "a"),
-                (XmlWriterEvent.START, "a"),
-                (XmlWriterEvent.ATTR, QNames.XSI_NIL, "true"),
-                (XmlWriterEvent.DATA, "0"),
-                (XmlWriterEvent.END, "a"),
-                (XmlWriterEvent.START, "a"),
-                (XmlWriterEvent.ATTR, QNames.XSI_NIL, "true"),
-                (XmlWriterEvent.DATA, ""),
-                (XmlWriterEvent.END, "a"),
-                (XmlWriterEvent.START, "a"),
-                (XmlWriterEvent.ATTR, QNames.XSI_NIL, "true"),
-                (XmlWriterEvent.DATA, None),
-                (XmlWriterEvent.END, "a"),
-                (XmlWriterEvent.START, "a"),
-                (XmlWriterEvent.ATTR, QNames.XSI_NIL, "true"),
-                (XmlWriterEvent.DATA, [""]),
-                (XmlWriterEvent.END, "a"),
-                (XmlWriterEvent.START, "a"),
-                (XmlWriterEvent.ATTR, QNames.XSI_NIL, "true"),
-                (XmlWriterEvent.DATA, []),
-                (XmlWriterEvent.END, "a"),
-                (XmlWriterEvent.END, "root"),
+                ("start", "root"),
+                ("start", "a"),
+                ("attr", QNames.XSI_NIL, "true"),
+                ("end", "a"),
+                ("start", "a"),
+                ("attr", QNames.XSI_NIL, "true"),
+                ("data", "0"),
+                ("end", "a"),
+                ("start", "a"),
+                ("attr", QNames.XSI_NIL, "true"),
+                ("data", ""),
+                ("end", "a"),
+                ("start", "a"),
+                ("attr", QNames.XSI_NIL, "true"),
+                ("data", None),
+                ("end", "a"),
+                ("start", "a"),
+                ("attr", QNames.XSI_NIL, "true"),
+                ("data", [""]),
+                ("end", "a"),
+                ("start", "a"),
+                ("attr", QNames.XSI_NIL, "true"),
+                ("data", []),
+                ("end", "a"),
+                ("end", "root"),
             ]
         )
         self.writer.write(events)
@@ -147,14 +160,14 @@ class XmlWriterTests(TestCase):
 
         self.assertEqual(expected, lines[1])
 
-    def test_write_resets_default_namespace_for_unqualified_elements(self):
+    def test_convert_resets_default_namespace_for_unqualified_elements(self):
         events = iter(
             [
-                (XmlWriterEvent.START, "{a}a"),
-                (XmlWriterEvent.START, "b"),
-                (XmlWriterEvent.DATA, "foo"),
-                (XmlWriterEvent.START, "b"),
-                (XmlWriterEvent.END, "{a}a"),
+                ("start", "{a}a"),
+                ("start", "b"),
+                ("data", "foo"),
+                ("start", "b"),
+                ("end", "{a}a"),
             ]
         )
 
@@ -193,3 +206,716 @@ class XmlWriterTests(TestCase):
         self.assertTrue(self.writer.is_xsi_type(QNames.XSI_TYPE, "{b}a"))
         self.assertFalse(self.writer.is_xsi_type("type", "{b}a"))
         self.assertTrue(self.writer.is_xsi_type("type", str(DataType.STRING)))
+
+
+class EventGeneratorTests(TestCase):
+    def setUp(self) -> None:
+        self.generator = EventGenerator()
+
+    def test_with_primitive_wrapper(self):
+        @dataclass
+        class PrimitiveWrapper:
+            primitive_list: List[str] = field(
+                metadata={
+                    "wrapper": "PrimitiveList",
+                    "type": "Element",
+                    "name": "Value",
+                }
+            )
+
+        obj = PrimitiveWrapper(primitive_list=["Value 1", "Value 2"])
+        events = self.generator.generate(obj)
+        expected = [
+            ("start", "PrimitiveWrapper"),
+            ("start", "PrimitiveList"),
+            ("start", "Value"),
+            ("data", "Value 1"),
+            ("end", "Value"),
+            ("start", "Value"),
+            ("data", "Value 2"),
+            ("end", "Value"),
+            ("end", "PrimitiveList"),
+            ("end", "PrimitiveWrapper"),
+        ]
+        self.assertEqual(expected, list(events))
+
+    def test_with_wrapper_element(self):
+        @dataclass
+        class ElementObject:
+            content: str = field(metadata={"type": "Element"})
+
+        @dataclass
+        class ElementWrapper:
+            elements: List[ElementObject] = field(
+                metadata={"wrapper": "Elements", "type": "Element", "name": "Object"}
+            )
+
+        obj = ElementWrapper(
+            elements=[ElementObject(content="Hello"), ElementObject(content="World")]
+        )
+        events = self.generator.generate(obj)
+        expected = [
+            ("start", "ElementWrapper"),
+            ("start", "Elements"),
+            ("start", "Object"),
+            ("start", "content"),
+            ("data", "Hello"),
+            ("end", "content"),
+            ("end", "Object"),
+            ("start", "Object"),
+            ("start", "content"),
+            ("data", "World"),
+            ("end", "content"),
+            ("end", "Object"),
+            ("end", "Elements"),
+            ("end", "ElementWrapper"),
+        ]
+        self.assertEqual(expected, list(events))
+
+    def test_with_wrapper_namespace(self):
+        @dataclass
+        class NamespaceWrapper:
+            items: List[str] = field(
+                metadata={
+                    "wrapper": "Items",
+                    "type": "Element",
+                    "name": "item",
+                    "namespace": "ns",
+                }
+            )
+
+        obj = NamespaceWrapper(items=["a", "b"])
+        events = self.generator.generate(obj)
+        expected = [
+            ("start", "NamespaceWrapper"),
+            ("start", "{ns}Items"),
+            ("start", "{ns}item"),
+            ("data", "a"),
+            ("end", "{ns}item"),
+            ("start", "{ns}item"),
+            ("data", "b"),
+            ("end", "{ns}item"),
+            ("end", "{ns}Items"),
+            ("end", "NamespaceWrapper"),
+        ]
+
+        self.assertEqual(expected, list(events))
+
+    def test_with_derived_element(self):
+        book = BookForm(id="123")
+        obj = DerivedElement(qname="item", value=book)
+
+        result = self.generator.generate(obj)
+        expected = [
+            ("start", "item"),
+            ("attr", "id", "123"),
+            ("attr", "lang", "en"),
+            ("attr", QNames.XSI_TYPE, "{urn:books}BookForm"),
+            ("end", "item"),
+        ]
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+        obj = DerivedElement(qname="{urn:books}BookForm", value=book)
+        result = self.generator.generate(obj)
+        expected = [
+            ("start", "{urn:books}BookForm"),
+            ("attr", "id", "123"),
+            ("attr", "lang", "en"),
+            ("end", "{urn:books}BookForm"),
+        ]
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_with_dataclass(self):
+        book = BookForm(id="123", title="Misterioso: A Crime Novel", price=19.5)
+        result = self.generator.generate(book)
+        expected = [
+            ("start", "BookForm"),
+            ("attr", "id", "123"),
+            ("attr", "lang", "en"),
+            ("start", "title"),
+            ("data", "Misterioso: A Crime Novel"),
+            ("end", "title"),
+            ("start", "price"),
+            ("data", "19.5"),
+            ("end", "price"),
+            ("end", "BookForm"),
+        ]
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_convert_dataclass_can_overwrite_params(self):
+        book = BookForm(id="123", title="Misterioso: A Crime Novel", price=19.5)
+        result = self.generator.convert_dataclass(
+            book, "xsdata", "book", True, "foo:book"
+        )
+        expected = [
+            ("start", "book"),
+            ("attr", "id", "123"),
+            ("attr", "lang", "en"),
+            ("attr", QNames.XSI_TYPE, "foo:book"),
+            ("attr", QNames.XSI_NIL, "true"),
+            ("start", "title"),
+            ("data", "Misterioso: A Crime Novel"),
+            ("end", "title"),
+            ("start", "price"),
+            ("data", "19.5"),
+            ("end", "price"),
+            ("end", "book"),
+        ]
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_convert_dataclass_with_no_dataclass(self):
+        with self.assertRaises(XmlContextError) as cm:
+            next(self.generator.convert_dataclass(1))
+        self.assertEqual("Type '<class 'int'>' is not a dataclass.", str(cm.exception))
+
+    def test_convert_mixed_content(self):
+        var = XmlVarFactory.create(xml_type=XmlType.WILDCARD, qname="a", mixed=True)
+        book = BookForm(id="123")
+        ebook = DerivedElement("ebook", BookForm(id="123"))
+        value = ["text", AnyElement(qname="br"), book, ebook, "tail"]
+        result = self.generator.convert_value(value, var, "xsdata")
+        expected = [
+            ("data", "text"),
+            ("start", "br"),
+            ("data", None),
+            ("end", "br"),
+            ("start", "{xsdata}BookForm"),
+            ("attr", "id", "123"),
+            ("attr", "lang", "en"),
+            ("end", "{xsdata}BookForm"),
+            ("start", "ebook"),
+            ("attr", "id", "123"),
+            ("attr", "lang", "en"),
+            ("attr", QNames.XSI_TYPE, "{urn:books}BookForm"),
+            ("end", "ebook"),
+            ("data", "tail"),
+        ]
+
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_convert_data(self):
+        var = XmlVarFactory.create(xml_type=XmlType.TEXT, qname="a")
+        expected = [("data", "123")]
+
+        result = self.generator.convert_value("123", var, "xsdata")
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_convert_tokens(self):
+        var = XmlVarFactory.create(
+            xml_type=XmlType.ELEMENT, qname="a", tokens_factory=list
+        )
+
+        result = self.generator.convert_value(None, var, "xsdata")
+        self.assertIsInstance(result, Generator)
+        self.assertListEqual([], list(result))
+
+        result = self.generator.convert_value([], var, "xsdata")
+        self.assertIsInstance(result, Generator)
+        self.assertListEqual([], list(result))
+
+        expected = [
+            ("start", "a"),
+            ("data", []),
+            ("end", "a"),
+        ]
+        var.required = True
+        result = self.generator.convert_value([], var, "xsdata")
+        self.assertIsInstance(result, Generator)
+        self.assertListEqual(expected, list(result))
+
+        expected = [
+            ("start", "a"),
+            ("data", ["1", QName("{a}b"), "3"]),
+            ("end", "a"),
+        ]
+        result = self.generator.convert_value([1, QName("{a}b"), 3], var, "xsdata")
+        self.assertEqual(expected, list(result))
+
+        expected = [
+            ("start", "a"),
+            ("data", ["1", "2", "3"]),
+            ("end", "a"),
+            ("start", "a"),
+            ("data", ["4", "5", "6"]),
+            ("end", "a"),
+        ]
+
+        result = self.generator.convert_value([[1, 2, 3], [4, 5, 6]], var, "xsdata")
+        self.assertEqual(expected, list(result))
+
+        var = XmlVarFactory.create(
+            xml_type=XmlType.ELEMENT, qname="a", tokens=True, nillable=True
+        )
+        expected = [
+            ("start", "a"),
+            ("attr", QNames.XSI_NIL, "true"),
+            ("data", []),
+            ("end", "a"),
+        ]
+
+        result = self.generator.convert_value([], var, "xsdata")
+        self.assertEqual(expected, list(result))
+
+    def test_convert_any_type_with_primitive(self):
+        var = XmlVarFactory.create(xml_type=XmlType.WILDCARD, qname="a")
+        expected = [("data", "str")]
+
+        result = self.generator.convert_value("str", var, "xsdata")
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_convert_any_type_with_primitive_element(self):
+        var = XmlVarFactory.create(xml_type=XmlType.ELEMENT, qname="a", types=(object,))
+        expected = [
+            ("start", "a"),
+            ("data", "str"),
+            ("end", "a"),
+        ]
+
+        result = self.generator.convert_value("str", var, "xsdata")
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_convert_any_type_with_any_element(self):
+        var = XmlVarFactory.create(xml_type=XmlType.WILDCARD, qname="a")
+        value = AnyElement(
+            qname="a",
+            text="b",
+            tail="c",
+            attributes={"d": 1, "e": 2},
+            children=[AnyElement(text="g"), "h"],
+        )
+        expected = [
+            ("start", "a"),
+            ("attr", "d", 1),
+            ("attr", "e", 2),
+            ("data", "b"),
+            ("data", "g"),
+            ("data", "h"),
+            ("end", "a"),
+            ("data", "c"),
+        ]
+
+        result = self.generator.convert_value(value, var, "xsdata")
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_convert_any_type_with_derived_element_primitive(self):
+        var = XmlVarFactory.create(xml_type=XmlType.WILDCARD, qname="a")
+        value = DerivedElement(qname="a", value=1)
+        expected = [
+            ("start", "a"),
+            ("attr", QNames.XSI_TYPE, QName(str(DataType.SHORT))),
+            ("data", 1),
+            ("end", "a"),
+        ]
+
+        result = self.generator.convert_value(value, var, "xsdata")
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_convert_any_type_with_derived_element_dataclass(self):
+        var = XmlVarFactory.create(xml_type=XmlType.WILDCARD, qname="a")
+        value = DerivedElement(
+            qname="a", value=BookForm(title="def"), type="{urn:books}BookForm"
+        )
+        expected = [
+            ("start", "a"),
+            ("attr", "lang", "en"),
+            ("attr", QNames.XSI_TYPE, QName("{urn:books}BookForm")),
+            ("start", "title"),
+            ("data", "def"),
+            ("end", "title"),
+            ("end", "a"),
+        ]
+
+        result = self.generator.convert_value(value, var, "xsdata")
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_convert_xsi_type(self):
+        var = XmlVarFactory.create(
+            xml_type=XmlType.ELEMENT,
+            qname="a",
+            name="a",
+            types=(BookForm,),
+        )
+        value = BookForm(id="123")
+        expected = [
+            ("start", "a"),
+            ("attr", "id", "123"),
+            ("attr", "lang", "en"),
+            ("end", "a"),
+        ]
+
+        result = self.generator.convert_value(value, var, "xsdata")
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_convert_xsi_type_with_derived_class(self):
+        var = XmlVarFactory.create(
+            xml_type=XmlType.ELEMENT,
+            qname="a",
+            name="a",
+            types=(BookForm,),
+        )
+        ebook = make_dataclass("eBook", [], bases=(BookForm,))
+        expected = [
+            ("start", "a"),
+            ("attr", "id", "123"),
+            ("attr", "lang", "en"),
+            ("attr", QNames.XSI_TYPE, QName("eBook")),
+            ("end", "a"),
+        ]
+
+        result = self.generator.convert_value(ebook(id="123"), var, "xsdata")
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_convert_xsi_type_with_illegal_derived_class(self):
+        var = XmlVarFactory.create(
+            xml_type=XmlType.ELEMENT,
+            qname="a",
+            name="a",
+            types=(BookForm,),
+        )
+        value = TypeA(1)
+
+        result = self.generator.convert_value(value, var, "xsdata")
+        with self.assertRaises(SerializerError) as cm:
+            list(result)
+
+        self.assertEqual("TypeA is not derived from BookForm", str(cm.exception))
+
+    def test_convert_element(self):
+        var = XmlVarFactory.create(xml_type=XmlType.ELEMENT, qname="a")
+        expected = [
+            ("start", "a"),
+            ("data", "123"),
+            ("end", "a"),
+        ]
+
+        result = self.generator.convert_value("123", var, "xsdata")
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_convert_element_with_nillable_true(self):
+        var = XmlVarFactory.create(xml_type=XmlType.ELEMENT, qname="a", nillable=True)
+        expected = [
+            ("start", "a"),
+            ("attr", QNames.XSI_NIL, "true"),
+            ("data", "123"),
+            ("end", "a"),
+        ]
+
+        result = self.generator.convert_value("123", var, "xsdata")
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_convert_element_with_any_type_var(self):
+        var = XmlVarFactory.create(
+            xml_type=XmlType.ELEMENT,
+            qname="a",
+            name="a",
+            types=(object,),
+            any_type=True,
+        )
+        expected = [
+            ("start", "a"),
+            ("attr", QNames.XSI_TYPE, QName(str(DataType.SHORT))),
+            ("data", "123"),
+            ("end", "a"),
+        ]
+
+        result = self.generator.convert_value(123, var, "xsdata")
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_convert_element_with_any_type_var_ignore_xs_string(self):
+        var = XmlVarFactory.create(
+            xml_type=XmlType.ELEMENT,
+            qname="a",
+            name="a",
+            types=(object,),
+            any_type=True,
+        )
+        expected = [
+            ("start", "a"),
+            ("data", ""),
+            ("end", "a"),
+        ]
+        result = self.generator.convert_value("", var, "xsdata")
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+        expected = [
+            ("start", "a"),
+            ("data", "123"),
+            ("end", "a"),
+        ]
+        result = self.generator.convert_value("123", var, "xsdata")
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_convert_choice_with_derived_primitive_value(self):
+        var = XmlVarFactory.create(
+            xml_type=XmlType.ELEMENTS,
+            name="compound",
+            qname="compound",
+            elements={
+                "a": XmlVarFactory.create(
+                    xml_type=XmlType.ELEMENT, qname="a", types=(int,)
+                )
+            },
+        )
+        value = DerivedElement(qname="a", value=1)
+        expected = [
+            ("start", "a"),
+            ("data", "1"),
+            ("end", "a"),
+        ]
+
+        result = self.generator.convert_value(value, var, "xsdata")
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_convert_choice_with_derived_dataclass(self):
+        var = XmlVarFactory.create(
+            xml_type=XmlType.ELEMENTS,
+            name="compound",
+            elements={
+                "a": XmlVarFactory.create(
+                    xml_type=XmlType.ELEMENT, qname="a", types=(SequentialType,)
+                )
+            },
+        )
+        value = DerivedElement(qname="a", value=SequentialType("foo"))
+        expected = [
+            ("start", "a"),
+            ("attr", "a0", "foo"),
+            ("end", "a"),
+        ]
+
+        result = self.generator.convert_value(value, var, "xsdata")
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_convert_choice_with_generic_object(self):
+        var = XmlVarFactory.create(
+            xml_type=XmlType.ELEMENTS,
+            name="compound",
+            qname="compound",
+            elements={
+                "a": XmlVarFactory.create(
+                    xml_type=XmlType.ELEMENT, qname="a", types=(int,)
+                )
+            },
+        )
+        value = AnyElement(qname="a", text="1")
+        expected = [
+            ("start", "a"),
+            ("data", "1"),
+            ("end", "a"),
+        ]
+
+        result = self.generator.convert_value(value, var, "xsdata")
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_convert_choice_with_raw_value(self):
+        var = XmlVarFactory.create(
+            xml_type=XmlType.ELEMENTS,
+            name="compound",
+            qname="compound",
+            elements={
+                "a": XmlVarFactory.create(
+                    xml_type=XmlType.ELEMENT, qname="a", types=(int,)
+                ),
+                "b": XmlVarFactory.create(
+                    xml_type=XmlType.ELEMENT,
+                    qname="b",
+                    name="b",
+                    types=(int,),
+                    tokens_factory=list,
+                ),
+            },
+        )
+        expected = [
+            ("start", "a"),
+            ("data", "1"),
+            ("end", "a"),
+            ("start", "b"),
+            ("data", ["1", "2"]),
+            ("end", "b"),
+        ]
+
+        result = self.generator.convert_value([1, [1, 2]], var, "xsdata")
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_convert_choice_when_no_matching_choice_exists_but_value_is_model(self):
+        var = XmlVarFactory.create(
+            xml_type=XmlType.ELEMENTS,
+            name="compound",
+            qname="compound",
+            elements={
+                "a": XmlVarFactory.create(
+                    xml_type=XmlType.WILDCARD, qname="a", types=(object,)
+                )
+            },
+        )
+
+        ebook = make_dataclass("eBook", [], bases=(BookForm,))
+        expected = [
+            ("start", "eBook"),
+            ("attr", "lang", "en"),
+            ("end", "eBook"),
+        ]
+
+        result = self.generator.convert_value(ebook(), var, None)
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_convert_choice_when_no_matching_choice_exists(self):
+        var = XmlVarFactory.create(
+            xml_type=XmlType.ELEMENTS,
+            name="compound",
+            qname="compound",
+            elements={
+                "a": XmlVarFactory.create(
+                    xml_type=XmlType.ELEMENT, qname="a", types=(float,)
+                )
+            },
+        )
+
+        with self.assertRaises(SerializerError) as cm:
+            result = self.generator.convert_value(1, var, "xsdata")
+            next(result)
+
+        msg = "XmlElements undefined choice: `compound` for `<class 'int'>`"
+        self.assertEqual(msg, str(cm.exception))
+
+    def test_convert_value_with_list_value(self):
+        var = XmlVarFactory.create(xml_type=XmlType.ELEMENT, qname="a", factory=list)
+        value = [True, False]
+        expected = [
+            ("start", "a"),
+            ("data", "true"),
+            ("end", "a"),
+            ("start", "a"),
+            ("data", "false"),
+            ("end", "a"),
+        ]
+
+        result = self.generator.convert_value(value, var, "xsdata")
+        self.assertIsInstance(result, Generator)
+        self.assertEqual(expected, list(result))
+
+    def test_next_value(self):
+        obj = SequentialType(x0=1, x1=[2, 3, 4, None], x2=[6, 7], x3=[9], x4=10)
+        meta = self.generator.context.build(SequentialType)
+        x0 = meta.text
+        x1 = next(meta.find_children("x1"))
+        x2 = next(meta.find_children("x2"))
+        x3 = next(meta.find_children("x3"))
+        x4 = next(meta.find_children("x4"))
+
+        actual = self.generator.next_value(obj, meta)
+        expected = [
+            (x0, 1),
+            (x1, 2),
+            (x2, 6),
+            (x1, 3),
+            (x2, 7),
+            (x1, 4),
+            (x3, 9),
+            (x4, 10),
+        ]
+
+        self.assertIsInstance(actual, Generator)
+        self.assertEqual(expected, list(actual))
+
+    def test_next_attribute(self):
+        obj = SequentialType(a0="foo", a1={"b": "c", "d": "e"})
+        meta = self.generator.context.build(SequentialType)
+
+        actual = self.generator.next_attribute(obj, meta, False, None, False)
+        expected = [
+            ("a0", "foo"),
+            ("b", "c"),
+            ("d", "e"),
+        ]
+
+        self.assertIsInstance(actual, Generator)
+        self.assertEqual(expected, list(actual))
+
+        actual = self.generator.next_attribute(obj, meta, True, "xs:bool", False)
+        expected.extend(
+            [
+                (QNames.XSI_TYPE, "xs:bool"),
+                (QNames.XSI_NIL, "true"),
+            ]
+        )
+        self.assertEqual(expected, list(actual))
+
+        meta.attributes["a0"].required = False
+        meta.attributes["a0"].default = "foo"
+        actual = self.generator.next_attribute(obj, meta, False, None, True)
+        expected = [
+            ("b", "c"),
+            ("d", "e"),
+        ]
+
+        self.assertIsInstance(actual, Generator)
+        self.assertEqual(expected, list(actual))
+
+    def test_with_mixed_content(self):
+        obj = Paragraph()
+        obj.content.append(AnyElement(qname="b", text="Mr."))
+        obj.content.append(Span("chris"))
+        obj.content.append("!")
+
+        result = self.generator.generate(obj)
+        expected = [
+            ("start", "p"),
+            ("start", "b"),
+            ("data", "Mr."),
+            ("end", "b"),
+            ("start", "span"),
+            ("data", "chris"),
+            ("end", "span"),
+            ("data", "!"),
+            ("end", "p"),
+        ]
+        self.assertEqual(expected, list(result))
+
+        obj = Paragraph()
+        obj.content.append("Hi ")
+        obj.content.append(AnyElement(qname="b", text="Mr."))
+        obj.content.append(Span("chris"))
+        obj.content.append("!")
+        result = self.generator.generate(obj)
+        expected = [
+            ("start", "p"),
+            ("data", "Hi "),
+            ("start", "b"),
+            ("data", "Mr."),
+            ("end", "b"),
+            ("start", "span"),
+            ("data", "chris"),
+            ("end", "span"),
+            ("data", "!"),
+            ("end", "p"),
+        ]
+        self.assertEqual(expected, list(result))
+
+    def test_encode_primitive_with_namedtuple(self):
+        var = XmlVarFactory.create(types=(Telephone,))
+        actual = XmlSerializer.encode_primitive(Telephone(30, 234, 56783), var)
+        self.assertEqual("30-234-56783", actual)
diff --git a/tests/formats/dataclass/serializers/test_xml.py b/tests/formats/dataclass/serializers/test_xml.py
index a8904730..54c924b7 100644
--- a/tests/formats/dataclass/serializers/test_xml.py
+++ b/tests/formats/dataclass/serializers/test_xml.py
@@ -1,670 +1,38 @@
-import re
-from dataclasses import dataclass, field, make_dataclass
-from typing import Generator, List
 from unittest import TestCase
-from xml.etree.ElementTree import QName
 
-from tests.fixtures.books import BookForm
-from tests.fixtures.datatypes import Telephone
-from tests.fixtures.models import Paragraph, SequentialType, Span, TypeA
-from xsdata.exceptions import SerializerError, XmlContextError
-from xsdata.formats.dataclass.models.elements import XmlType
-from xsdata.formats.dataclass.models.generics import AnyElement, DerivedElement
+from tests.fixtures.books.fixtures import books
 from xsdata.formats.dataclass.serializers import XmlSerializer
-from xsdata.formats.dataclass.serializers.mixins import XmlWriterEvent
-from xsdata.models.enums import DataType, QNames
-from xsdata.utils.testing import XmlVarFactory
+from xsdata.formats.dataclass.serializers.config import SerializerConfig
 
 
 class XmlSerializerTests(TestCase):
-    def setUp(self) -> None:
-        self.serializer = XmlSerializer()
-
-    def test_wrapper_primitive(self):
-        @dataclass
-        class PrimitiveWrapper:
-            primitive_list: List[str] = field(
-                metadata={
-                    "wrapper": "PrimitiveList",
-                    "type": "Element",
-                    "name": "Value",
-                }
-            )
-
-        obj = PrimitiveWrapper(primitive_list=["Value 1", "Value 2"])
-        xml = self.serializer.render(obj)
-        expected = r"<PrimitiveWrapper><PrimitiveList><Value>Value 1</Value><Value>Value 2</Value></PrimitiveList></PrimitiveWrapper>"
-        self.assertIsNotNone(re.search(expected, xml))
-
-    def test_wrapper_element(self):
-        @dataclass
-        class ElementObject:
-            content: str = field(metadata={"type": "Element"})
-
-        @dataclass
-        class ElementWrapper:
-            elements: List[ElementObject] = field(
-                metadata={"wrapper": "Elements", "type": "Element", "name": "Object"}
-            )
-
-        obj = ElementWrapper(
-            elements=[ElementObject(content="Hello"), ElementObject(content="World")]
-        )
-        xml = self.serializer.render(obj)
-        expected = "<ElementWrapper><Elements><Object><content>Hello</content></Object><Object><content>World</content></Object></Elements></ElementWrapper>"
-        self.assertIsNotNone(re.search(expected, xml))
-
-    def test_wrapper_namespace(self):
-        @dataclass
-        class NamespaceWrapper:
-            items: List[str] = field(
-                metadata={
-                    "wrapper": "Items",
-                    "type": "Element",
-                    "name": "item",
-                    "namespace": "ns",
-                }
-            )
-
-        ns_map = {"foo": "ns"}
-        obj = NamespaceWrapper(items=["a", "b"])
-        xml = self.serializer.render(obj, ns_map=ns_map)
-        expected = '<NamespaceWrapper xmlns:foo="ns"><foo:Items><foo:item>a</foo:item><foo:item>b</foo:item></foo:Items></NamespaceWrapper>'
-        self.assertIsNotNone(re.search(expected, xml))
-
-    def test_write_object_with_derived_element(self):
-        book = BookForm(id="123")
-        obj = DerivedElement(qname="item", value=book)
-
-        result = self.serializer.write_object(obj)
-        expected = [
-            (XmlWriterEvent.START, "item"),
-            (XmlWriterEvent.ATTR, "id", "123"),
-            (XmlWriterEvent.ATTR, "lang", "en"),
-            (XmlWriterEvent.ATTR, QNames.XSI_TYPE, "{urn:books}BookForm"),
-            (XmlWriterEvent.END, "item"),
-        ]
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-        obj = DerivedElement(qname="{urn:books}BookForm", value=book)
-        result = self.serializer.write_object(obj)
-        expected = [
-            (XmlWriterEvent.START, "{urn:books}BookForm"),
-            (XmlWriterEvent.ATTR, "id", "123"),
-            (XmlWriterEvent.ATTR, "lang", "en"),
-            (XmlWriterEvent.END, "{urn:books}BookForm"),
-        ]
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_write_dataclass(self):
-        book = BookForm(id="123", title="Misterioso: A Crime Novel", price=19.5)
-        result = self.serializer.write_object(book)
-        expected = [
-            (XmlWriterEvent.START, "BookForm"),
-            (XmlWriterEvent.ATTR, "id", "123"),
-            (XmlWriterEvent.ATTR, "lang", "en"),
-            (XmlWriterEvent.START, "title"),
-            (XmlWriterEvent.DATA, "Misterioso: A Crime Novel"),
-            (XmlWriterEvent.END, "title"),
-            (XmlWriterEvent.START, "price"),
-            (XmlWriterEvent.DATA, "19.5"),
-            (XmlWriterEvent.END, "price"),
-            (XmlWriterEvent.END, "BookForm"),
-        ]
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_write_dataclass_can_overwrite_params(self):
-        book = BookForm(id="123", title="Misterioso: A Crime Novel", price=19.5)
-        result = self.serializer.write_dataclass(
-            book, "xsdata", "book", True, "foo:book"
-        )
-        expected = [
-            (XmlWriterEvent.START, "book"),
-            (XmlWriterEvent.ATTR, "id", "123"),
-            (XmlWriterEvent.ATTR, "lang", "en"),
-            (XmlWriterEvent.ATTR, QNames.XSI_TYPE, "foo:book"),
-            (XmlWriterEvent.ATTR, QNames.XSI_NIL, "true"),
-            (XmlWriterEvent.START, "title"),
-            (XmlWriterEvent.DATA, "Misterioso: A Crime Novel"),
-            (XmlWriterEvent.END, "title"),
-            (XmlWriterEvent.START, "price"),
-            (XmlWriterEvent.DATA, "19.5"),
-            (XmlWriterEvent.END, "price"),
-            (XmlWriterEvent.END, "book"),
-        ]
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_write_dataclass_with_no_dataclass(self):
-        with self.assertRaises(XmlContextError) as cm:
-            next(self.serializer.write_dataclass(1))
-        self.assertEqual("Type '<class 'int'>' is not a dataclass.", str(cm.exception))
-
-    def test_write_mixed_content(self):
-        var = XmlVarFactory.create(xml_type=XmlType.WILDCARD, qname="a", mixed=True)
-        book = BookForm(id="123")
-        ebook = DerivedElement("ebook", BookForm(id="123"))
-        value = ["text", AnyElement(qname="br"), book, ebook, "tail"]
-        result = self.serializer.write_value(value, var, "xsdata")
-        expected = [
-            (XmlWriterEvent.DATA, "text"),
-            (XmlWriterEvent.START, "br"),
-            (XmlWriterEvent.DATA, None),
-            (XmlWriterEvent.END, "br"),
-            (XmlWriterEvent.START, "{xsdata}BookForm"),
-            (XmlWriterEvent.ATTR, "id", "123"),
-            (XmlWriterEvent.ATTR, "lang", "en"),
-            (XmlWriterEvent.END, "{xsdata}BookForm"),
-            (XmlWriterEvent.START, "ebook"),
-            (XmlWriterEvent.ATTR, "id", "123"),
-            (XmlWriterEvent.ATTR, "lang", "en"),
-            ("attr", QNames.XSI_TYPE, "{urn:books}BookForm"),
-            (XmlWriterEvent.END, "ebook"),
-            (XmlWriterEvent.DATA, "tail"),
-        ]
-
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_write_data(self):
-        var = XmlVarFactory.create(xml_type=XmlType.TEXT, qname="a")
-        expected = [(XmlWriterEvent.DATA, "123")]
-
-        result = self.serializer.write_value("123", var, "xsdata")
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_write_tokens(self):
-        var = XmlVarFactory.create(
-            xml_type=XmlType.ELEMENT, qname="a", tokens_factory=list
-        )
-
-        result = self.serializer.write_value(None, var, "xsdata")
-        self.assertIsInstance(result, Generator)
-        self.assertListEqual([], list(result))
-
-        result = self.serializer.write_value([], var, "xsdata")
-        self.assertIsInstance(result, Generator)
-        self.assertListEqual([], list(result))
-
-        expected = [
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.DATA, []),
-            (XmlWriterEvent.END, "a"),
-        ]
-        var.required = True
-        result = self.serializer.write_value([], var, "xsdata")
-        self.assertIsInstance(result, Generator)
-        self.assertListEqual(expected, list(result))
-
-        expected = [
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.DATA, ["1", QName("{a}b"), "3"]),
-            (XmlWriterEvent.END, "a"),
-        ]
-        result = self.serializer.write_value([1, QName("{a}b"), 3], var, "xsdata")
-        self.assertEqual(expected, list(result))
-
-        expected = [
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.DATA, ["1", "2", "3"]),
-            (XmlWriterEvent.END, "a"),
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.DATA, ["4", "5", "6"]),
-            (XmlWriterEvent.END, "a"),
-        ]
-
-        result = self.serializer.write_value([[1, 2, 3], [4, 5, 6]], var, "xsdata")
-        self.assertEqual(expected, list(result))
-
-        var = XmlVarFactory.create(
-            xml_type=XmlType.ELEMENT, qname="a", tokens=True, nillable=True
-        )
-        expected = [
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.ATTR, QNames.XSI_NIL, "true"),
-            (XmlWriterEvent.DATA, []),
-            (XmlWriterEvent.END, "a"),
-        ]
-
-        result = self.serializer.write_value([], var, "xsdata")
-        self.assertEqual(expected, list(result))
-
-    def test_write_any_type_with_primitive(self):
-        var = XmlVarFactory.create(xml_type=XmlType.WILDCARD, qname="a")
-        expected = [(XmlWriterEvent.DATA, "str")]
-
-        result = self.serializer.write_value("str", var, "xsdata")
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_write_any_type_with_primitive_element(self):
-        var = XmlVarFactory.create(xml_type=XmlType.ELEMENT, qname="a", types=(object,))
-        expected = [
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.DATA, "str"),
-            (XmlWriterEvent.END, "a"),
-        ]
-
-        result = self.serializer.write_value("str", var, "xsdata")
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_write_any_type_with_any_element(self):
-        var = XmlVarFactory.create(xml_type=XmlType.WILDCARD, qname="a")
-        value = AnyElement(
-            qname="a",
-            text="b",
-            tail="c",
-            attributes={"d": 1, "e": 2},
-            children=[AnyElement(text="g"), "h"],
-        )
-        expected = [
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.ATTR, "d", 1),
-            (XmlWriterEvent.ATTR, "e", 2),
-            (XmlWriterEvent.DATA, "b"),
-            (XmlWriterEvent.DATA, "g"),
-            (XmlWriterEvent.DATA, "h"),
-            (XmlWriterEvent.END, "a"),
-            (XmlWriterEvent.DATA, "c"),
-        ]
-
-        result = self.serializer.write_value(value, var, "xsdata")
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_write_any_type_with_derived_element_primitive(self):
-        var = XmlVarFactory.create(xml_type=XmlType.WILDCARD, qname="a")
-        value = DerivedElement(qname="a", value=1)
-        expected = [
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.ATTR, QNames.XSI_TYPE, QName(str(DataType.SHORT))),
-            (XmlWriterEvent.DATA, 1),
-            (XmlWriterEvent.END, "a"),
-        ]
-
-        result = self.serializer.write_value(value, var, "xsdata")
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_write_any_type_with_derived_element_dataclass(self):
-        var = XmlVarFactory.create(xml_type=XmlType.WILDCARD, qname="a")
-        value = DerivedElement(
-            qname="a", value=BookForm(title="def"), type="{urn:books}BookForm"
-        )
-        expected = [
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.ATTR, "lang", "en"),
-            (XmlWriterEvent.ATTR, QNames.XSI_TYPE, QName("{urn:books}BookForm")),
-            (XmlWriterEvent.START, "title"),
-            (XmlWriterEvent.DATA, "def"),
-            (XmlWriterEvent.END, "title"),
-            (XmlWriterEvent.END, "a"),
-        ]
-
-        result = self.serializer.write_value(value, var, "xsdata")
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_write_xsi_type(self):
-        var = XmlVarFactory.create(
-            xml_type=XmlType.ELEMENT,
-            qname="a",
-            name="a",
-            types=(BookForm,),
-        )
-        value = BookForm(id="123")
-        expected = [
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.ATTR, "id", "123"),
-            (XmlWriterEvent.ATTR, "lang", "en"),
-            (XmlWriterEvent.END, "a"),
-        ]
-
-        result = self.serializer.write_value(value, var, "xsdata")
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_write_xsi_type_with_derived_class(self):
-        var = XmlVarFactory.create(
-            xml_type=XmlType.ELEMENT,
-            qname="a",
-            name="a",
-            types=(BookForm,),
-        )
-        ebook = make_dataclass("eBook", [], bases=(BookForm,))
-        expected = [
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.ATTR, "id", "123"),
-            (XmlWriterEvent.ATTR, "lang", "en"),
-            (XmlWriterEvent.ATTR, QNames.XSI_TYPE, QName("eBook")),
-            (XmlWriterEvent.END, "a"),
-        ]
-
-        result = self.serializer.write_value(ebook(id="123"), var, "xsdata")
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_write_xsi_type_with_illegal_derived_class(self):
-        var = XmlVarFactory.create(
-            xml_type=XmlType.ELEMENT,
-            qname="a",
-            name="a",
-            types=(BookForm,),
-        )
-        value = TypeA(1)
-
-        result = self.serializer.write_value(value, var, "xsdata")
-        with self.assertRaises(SerializerError) as cm:
-            list(result)
-
-        self.assertEqual("TypeA is not derived from BookForm", str(cm.exception))
-
-    def test_write_element(self):
-        var = XmlVarFactory.create(xml_type=XmlType.ELEMENT, qname="a")
-        expected = [
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.DATA, "123"),
-            (XmlWriterEvent.END, "a"),
-        ]
-
-        result = self.serializer.write_value("123", var, "xsdata")
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_write_element_with_nillable_true(self):
-        var = XmlVarFactory.create(xml_type=XmlType.ELEMENT, qname="a", nillable=True)
-        expected = [
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.ATTR, QNames.XSI_NIL, "true"),
-            (XmlWriterEvent.DATA, "123"),
-            (XmlWriterEvent.END, "a"),
-        ]
-
-        result = self.serializer.write_value("123", var, "xsdata")
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_write_element_with_any_type_var(self):
-        var = XmlVarFactory.create(
-            xml_type=XmlType.ELEMENT,
-            qname="a",
-            name="a",
-            types=(object,),
-            any_type=True,
-        )
-        expected = [
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.ATTR, QNames.XSI_TYPE, QName(str(DataType.SHORT))),
-            (XmlWriterEvent.DATA, "123"),
-            (XmlWriterEvent.END, "a"),
-        ]
-
-        result = self.serializer.write_value(123, var, "xsdata")
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_write_element_with_any_type_var_ignore_xs_string(self):
-        var = XmlVarFactory.create(
-            xml_type=XmlType.ELEMENT,
-            qname="a",
-            name="a",
-            types=(object,),
-            any_type=True,
-        )
-        expected = [
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.DATA, ""),
-            (XmlWriterEvent.END, "a"),
-        ]
-        result = self.serializer.write_value("", var, "xsdata")
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-        expected = [
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.DATA, "123"),
-            (XmlWriterEvent.END, "a"),
-        ]
-        result = self.serializer.write_value("123", var, "xsdata")
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_write_choice_with_derived_primitive_value(self):
-        var = XmlVarFactory.create(
-            xml_type=XmlType.ELEMENTS,
-            name="compound",
-            qname="compound",
-            elements={
-                "a": XmlVarFactory.create(
-                    xml_type=XmlType.ELEMENT, qname="a", types=(int,)
-                )
-            },
-        )
-        value = DerivedElement(qname="a", value=1)
-        expected = [
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.DATA, "1"),
-            (XmlWriterEvent.END, "a"),
-        ]
-
-        result = self.serializer.write_value(value, var, "xsdata")
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_write_choice_with_derived_dataclass(self):
-        var = XmlVarFactory.create(
-            xml_type=XmlType.ELEMENTS,
-            name="compound",
-            elements={
-                "a": XmlVarFactory.create(
-                    xml_type=XmlType.ELEMENT, qname="a", types=(SequentialType,)
-                )
-            },
-        )
-        value = DerivedElement(qname="a", value=SequentialType("foo"))
-        expected = [
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.ATTR, "a0", "foo"),
-            (XmlWriterEvent.END, "a"),
-        ]
-
-        result = self.serializer.write_value(value, var, "xsdata")
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_write_choice_with_generic_object(self):
-        var = XmlVarFactory.create(
-            xml_type=XmlType.ELEMENTS,
-            name="compound",
-            qname="compound",
-            elements={
-                "a": XmlVarFactory.create(
-                    xml_type=XmlType.ELEMENT, qname="a", types=(int,)
-                )
-            },
-        )
-        value = AnyElement(qname="a", text="1")
-        expected = [
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.DATA, "1"),
-            (XmlWriterEvent.END, "a"),
-        ]
-
-        result = self.serializer.write_value(value, var, "xsdata")
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_write_choice_with_raw_value(self):
-        var = XmlVarFactory.create(
-            xml_type=XmlType.ELEMENTS,
-            name="compound",
-            qname="compound",
-            elements={
-                "a": XmlVarFactory.create(
-                    xml_type=XmlType.ELEMENT, qname="a", types=(int,)
-                ),
-                "b": XmlVarFactory.create(
-                    xml_type=XmlType.ELEMENT,
-                    qname="b",
-                    name="b",
-                    types=(int,),
-                    tokens_factory=list,
-                ),
-            },
-        )
-        expected = [
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.DATA, "1"),
-            (XmlWriterEvent.END, "a"),
-            (XmlWriterEvent.START, "b"),
-            (XmlWriterEvent.DATA, ["1", "2"]),
-            (XmlWriterEvent.END, "b"),
-        ]
-
-        result = self.serializer.write_value([1, [1, 2]], var, "xsdata")
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_write_choice_when_no_matching_choice_exists_but_value_is_model(self):
-        var = XmlVarFactory.create(
-            xml_type=XmlType.ELEMENTS,
-            name="compound",
-            qname="compound",
-            elements={
-                "a": XmlVarFactory.create(
-                    xml_type=XmlType.WILDCARD, qname="a", types=(object,)
-                )
-            },
-        )
-
-        ebook = make_dataclass("eBook", [], bases=(BookForm,))
-        expected = [
-            (XmlWriterEvent.START, "eBook"),
-            (XmlWriterEvent.ATTR, "lang", "en"),
-            (XmlWriterEvent.END, "eBook"),
-        ]
-
-        result = self.serializer.write_value(ebook(), var, None)
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_write_choice_when_no_matching_choice_exists(self):
-        var = XmlVarFactory.create(
-            xml_type=XmlType.ELEMENTS,
-            name="compound",
-            qname="compound",
-            elements={
-                "a": XmlVarFactory.create(
-                    xml_type=XmlType.ELEMENT, qname="a", types=(float,)
-                )
-            },
-        )
-
-        with self.assertRaises(SerializerError) as cm:
-            result = self.serializer.write_value(1, var, "xsdata")
-            next(result)
-
-        msg = "XmlElements undefined choice: `compound` for `<class 'int'>`"
-        self.assertEqual(msg, str(cm.exception))
-
-    def test_write_value_with_list_value(self):
-        var = XmlVarFactory.create(xml_type=XmlType.ELEMENT, qname="a", factory=list)
-        value = [True, False]
-        expected = [
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.DATA, "true"),
-            (XmlWriterEvent.END, "a"),
-            (XmlWriterEvent.START, "a"),
-            (XmlWriterEvent.DATA, "false"),
-            (XmlWriterEvent.END, "a"),
-        ]
-
-        result = self.serializer.write_value(value, var, "xsdata")
-        self.assertIsInstance(result, Generator)
-        self.assertEqual(expected, list(result))
-
-    def test_next_value(self):
-        obj = SequentialType(x0=1, x1=[2, 3, 4, None], x2=[6, 7], x3=[9], x4=10)
-        meta = self.serializer.context.build(SequentialType)
-        x0 = meta.text
-        x1 = next(meta.find_children("x1"))
-        x2 = next(meta.find_children("x2"))
-        x3 = next(meta.find_children("x3"))
-        x4 = next(meta.find_children("x4"))
-
-        actual = self.serializer.next_value(obj, meta)
-        expected = [
-            (x0, 1),
-            (x1, 2),
-            (x2, 6),
-            (x1, 3),
-            (x2, 7),
-            (x1, 4),
-            (x3, 9),
-            (x4, 10),
-        ]
-
-        self.assertIsInstance(actual, Generator)
-        self.assertEqual(expected, list(actual))
-
-    def test_next_attribute(self):
-        obj = SequentialType(a0="foo", a1={"b": "c", "d": "e"})
-        meta = self.serializer.context.build(SequentialType)
-
-        actual = self.serializer.next_attribute(obj, meta, False, None, False)
-        expected = [
-            ("a0", "foo"),
-            ("b", "c"),
-            ("d", "e"),
-        ]
-
-        self.assertIsInstance(actual, Generator)
-        self.assertEqual(expected, list(actual))
-
-        actual = self.serializer.next_attribute(obj, meta, True, "xs:bool", False)
-        expected.extend(
-            [
-                (QNames.XSI_TYPE, "xs:bool"),
-                (QNames.XSI_NIL, "true"),
-            ]
-        )
-        self.assertEqual(expected, list(actual))
-
-        meta.attributes["a0"].required = False
-        meta.attributes["a0"].default = "foo"
-        actual = self.serializer.next_attribute(obj, meta, False, None, True)
-        expected = [
-            ("b", "c"),
-            ("d", "e"),
-        ]
-
-        self.assertIsInstance(actual, Generator)
-        self.assertEqual(expected, list(actual))
-
-    def test_render_mixed_content(self):
-        obj = Paragraph()
-        obj.content.append(AnyElement(qname="b", text="Mr."))
-        obj.content.append(Span("chris"))
-        obj.content.append("!")
-
-        self.serializer.pretty_print = False
-        result = self.serializer.render(obj).splitlines()
-        self.assertEqual("<p><b>Mr.</b><span>chris</span>!</p>", result[1])
-
-        obj = Paragraph()
-        obj.content.append("Hi ")
-        obj.content.append(AnyElement(qname="b", text="Mr."))
-        obj.content.append(Span("chris"))
-        obj.content.append("!")
-        result = self.serializer.render(obj).splitlines()
-        self.assertEqual("<p>Hi <b>Mr.</b><span>chris</span>!</p>", result[1])
-
-    def test_encode_namedtuple(self):
-        var = XmlVarFactory.create(types=(Telephone,))
-        actual = XmlSerializer.encode(Telephone(30, 234, 56783), var)
-        self.assertEqual("30-234-56783", actual)
+    def setUp(self):
+        config = SerializerConfig(indent="  ")
+        self.serializer = XmlSerializer(config=config)
+        super().setUp()
+
+    def test_render(self):
+        result = self.serializer.render(books, ns_map={None: "urn:books"})
+        expected = (
+            '<?xml version="1.0" encoding="UTF-8"?>\n'
+            '<books xmlns="urn:books">\n'
+            '  <book xmlns="" id="bk001" lang="en">\n'
+            "    <author>Hightower, Kim</author>\n"
+            "    <title>The First Book</title>\n"
+            "    <genre>Fiction</genre>\n"
+            "    <price>44.95</price>\n"
+            "    <pub_date>2000-10-01</pub_date>\n"
+            "    <review>An amazing story of nothing.</review>\n"
+            "  </book>\n"
+            '  <book xmlns="" id="bk002" lang="en">\n'
+            "    <author>Nagata, Suanne</author>\n"
+            "    <title>Becoming Somebody</title>\n"
+            "    <genre>Biography</genre>\n"
+            "    <price>33.95</price>\n"
+            "    <pub_date>2001-01-10</pub_date>\n"
+            "    <review>A masterpiece of the fine art of gossiping.</review>\n"
+            "  </book>\n"
+            "</books>\n"
+        )
+
+        self.assertEqual(expected, result)
diff --git a/tests/formats/dataclass/serializers/tree/__init__.py b/tests/formats/dataclass/serializers/tree/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/formats/dataclass/serializers/tree/test_lxml.py b/tests/formats/dataclass/serializers/tree/test_lxml.py
new file mode 100644
index 00000000..96a3e9c2
--- /dev/null
+++ b/tests/formats/dataclass/serializers/tree/test_lxml.py
@@ -0,0 +1,36 @@
+from unittest import TestCase
+
+from lxml import etree
+
+from tests.fixtures.books.fixtures import books
+from xsdata.formats.dataclass.serializers import LxmlTreeSerializer
+
+
+class LxmlTreeSerializerTests(TestCase):
+    def test_render(self):
+        serializer = LxmlTreeSerializer()
+        result = serializer.render(books)
+
+        etree.indent(result)
+        actual = etree.tostring(result)
+        expected = (
+            '<ns0:books xmlns:ns0="urn:books">\n'
+            "  <book>\n"
+            "    <author>Hightower, Kim</author>\n"
+            "    <title>The First Book</title>\n"
+            "    <genre>Fiction</genre>\n"
+            "    <price>44.95</price>\n"
+            "    <pub_date>2000-10-01</pub_date>\n"
+            "    <review>An amazing story of nothing.</review>\n"
+            "  </book>\n"
+            "  <book>\n"
+            "    <author>Nagata, Suanne</author>\n"
+            "    <title>Becoming Somebody</title>\n"
+            "    <genre>Biography</genre>\n"
+            "    <price>33.95</price>\n"
+            "    <pub_date>2001-01-10</pub_date>\n"
+            "    <review>A masterpiece of the fine art of gossiping.</review>\n"
+            "  </book>\n"
+            "</ns0:books>"
+        )
+        self.assertEqual(expected, actual.decode())
diff --git a/tests/formats/dataclass/serializers/tree/test_mixins.py b/tests/formats/dataclass/serializers/tree/test_mixins.py
new file mode 100644
index 00000000..eab86c50
--- /dev/null
+++ b/tests/formats/dataclass/serializers/tree/test_mixins.py
@@ -0,0 +1,21 @@
+from unittest import TestCase, mock
+from xml.etree.ElementTree import TreeBuilder
+
+from tests.fixtures.books.fixtures import books
+from xsdata.exceptions import XmlHandlerError
+from xsdata.formats.dataclass.serializers.tree.mixins import TreeSerializer
+
+
+class TreeSerializerTests(TestCase):
+    @mock.patch.object(TreeSerializer, "generate")
+    def test_build_with_unknown_event(self, mock_generate):
+        mock_generate.return_value = [
+            ("foobar", True),
+        ]
+
+        builder = TreeBuilder()
+        serializer = TreeSerializer()
+        with self.assertRaises(XmlHandlerError) as cm:
+            serializer.build(books, builder)
+
+        self.assertEqual("Unhandled event: `foobar`.", str(cm.exception))
diff --git a/tests/formats/dataclass/serializers/tree/test_native.py b/tests/formats/dataclass/serializers/tree/test_native.py
new file mode 100644
index 00000000..0da3cdb8
--- /dev/null
+++ b/tests/formats/dataclass/serializers/tree/test_native.py
@@ -0,0 +1,39 @@
+import sys
+from unittest import TestCase
+from xml.etree import ElementTree
+
+import pytest
+
+from tests.fixtures.books.fixtures import books
+from xsdata.formats.dataclass.serializers import XmlTreeSerializer
+
+
+class XmlTreeSerializerTests(TestCase):
+    @pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9")
+    def test_render(self):
+        serializer = XmlTreeSerializer()
+        result = serializer.render(books)
+
+        ElementTree.indent(result)
+        actual = ElementTree.tostring(result)
+        expected = (
+            '<ns0:books xmlns:ns0="urn:books">\n'
+            "  <book>\n"
+            "    <author>Hightower, Kim</author>\n"
+            "    <title>The First Book</title>\n"
+            "    <genre>Fiction</genre>\n"
+            "    <price>44.95</price>\n"
+            "    <pub_date>2000-10-01</pub_date>\n"
+            "    <review>An amazing story of nothing.</review>\n"
+            "  </book>\n"
+            "  <book>\n"
+            "    <author>Nagata, Suanne</author>\n"
+            "    <title>Becoming Somebody</title>\n"
+            "    <genre>Biography</genre>\n"
+            "    <price>33.95</price>\n"
+            "    <pub_date>2001-01-10</pub_date>\n"
+            "    <review>A masterpiece of the fine art of gossiping.</review>\n"
+            "  </book>\n"
+            "</ns0:books>"
+        )
+        self.assertEqual(expected, actual.decode())