Repository: tefra/xsdata (python)

Issue:
xsi:type not working on xsd:anyType elements
I have a use case were we wrap lists of 'items' in an xml where the elements are in the xsd marked as 'xs:anyType'.

The xml though specifies the type with something like:
` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="api:searchResultItem" `
so, the actual type is specified with an xsi:type attribute.

If I unmarshal this xml with xsdata these object just become `AnyElement`s and are not unmarshaled to the correct type (as e.g. pyxb and jaxb would do).

I don't rule out that I do something wrong, but as far as I looked into the code it is not supported at all. But please  correct me if I'm wrong.

Pull request:
Resolve #306 Lookup xsi type for xs:anyType derived elements
### Description

Currently fields derived from elements with type `xs:anyType` and fields derived from `xs:any` wildcards are using the same flow for parsing/serializing. This prevents any xsi:type lookups. 


###  What I did
- Introduced `AnyTypeNode` to split the flow from wildcards during parsing.
- The new node attempts to lookup for a dataclass that matches the xsi:type attribute
  - Exception one: simple types xs:int, xs:float, xs:int etc etc
  - Exception two no xsi:type or no match defaults back to the generic `AnyElement` like it used to
  - Exception three: the dataclass has to be already imported

The lookup process is cached but still quite aggressive, all imported dataclasses are indexed by their xsi:type qname


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 725224f8..71807971 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -7,7 +7,7 @@ repos:
       - id: pyupgrade
         args: [--py37-plus]
   - repo: https://github.com/asottile/reorder_python_imports
-    rev: v2.3.5
+    rev: v2.3.6
     hooks:
       - id: reorder-python-imports
   - repo: https://github.com/ambv/black
diff --git a/xsdata/formats/dataclass/context.py b/xsdata/formats/dataclass/context.py
index b730d1be..28a046e4 100644
--- a/xsdata/formats/dataclass/context.py
+++ b/xsdata/formats/dataclass/context.py
@@ -21,6 +21,7 @@ from xsdata.formats.converter import converter
 from xsdata.formats.dataclass.models.constants import XmlType
 from xsdata.formats.dataclass.models.elements import XmlMeta
 from xsdata.formats.dataclass.models.elements import XmlVar
+from xsdata.models.enums import DataType
 from xsdata.models.enums import NamespaceType
 from xsdata.utils.constants import EMPTY_SEQUENCE
 from xsdata.utils.namespaces import build_qname
@@ -34,12 +35,15 @@ class XmlContext:
 
     :param element_name: Default callable to convert field names to element tags
     :param attribute_name: Default callable to convert field names to attribute tags
-    :param cache: Local storage to store and reuse models' bind metadata.
+    :param cache: Cache models metadata
+    :param xsi_cache: Index models by xsi:type
     """
 
     element_name: Callable = field(default=lambda x: x)
     attribute_name: Callable = field(default=lambda x: x)
     cache: Dict[Type, XmlMeta] = field(default_factory=dict)
+    xsi_cache: Dict[str, List[Type]] = field(default_factory=lambda: defaultdict(list))
+    sys_modules: int = field(default=0, init=False)
 
     def fetch(
         self,
@@ -58,45 +62,60 @@ class XmlContext:
             name instead.
         """
         meta = self.build(clazz, parent_ns)
-
-        subclass = self.find_subclass(clazz, xsi_type) if xsi_type else None
-        if subclass:
-            meta = self.build(subclass, parent_ns)
-
-        return meta
-
-    def find_subclass(self, clazz: Type, xsi_type: str) -> Optional[Type]:
+        subclass = None
+        if xsi_type and meta.source_qname != xsi_type:
+            subclass = self.find_subclass(clazz, xsi_type)
+
+        return self.build(subclass, parent_ns) if subclass else meta
+
+    def build_xsi_cache(self):
+        """Index all imported dataclasses by their xsi:type qualified name."""
+        self.xsi_cache.clear()
+
+        for clazz in self.get_subclasses(object):
+            if is_dataclass(clazz):
+                meta = clazz.Meta if "Meta" in clazz.__dict__ else None
+                name = getattr(meta, "name", None) or self.local_name(clazz.__name__)
+                module = sys.modules[clazz.__module__]
+                source_namespace = getattr(module, "__NAMESPACE__", None)
+                source_qname = build_qname(source_namespace, name)
+                self.xsi_cache[source_qname].append(clazz)
+
+    def find_types(self, qname: str) -> Optional[List[Type]]:
         """
-        Find a derived class of the given clazz that matches the given
-        qualified xsi type.
+        Find all classes that match the given xsi:type qname.
 
-        The derived class is either a subclass or shares the same parent
-        class as the given class.
+        - Ignores native schema types, xs:string, xs:float, xs:int, ...
+        - Rebuild cache if new modules were imported since last run
         """
-        for subclass in clazz.__subclasses__():
-            if self.match_class_source_qname(subclass, xsi_type):
-                return subclass
+        if DataType.from_qname(qname):
+            return None
 
-        for base in clazz.__bases__:
-            if not is_dataclass(base):
-                continue
+        if len(sys.modules) != self.sys_modules:
+            self.build_xsi_cache()
+            self.sys_modules = len(sys.modules)
 
-            if self.match_class_source_qname(base, xsi_type):
-                return base
+        return self.xsi_cache[qname] if qname in self.xsi_cache else None
 
-            sibling = self.find_subclass(base, xsi_type)
-            if sibling:
-                return sibling
+    def find_type(self, qname: str) -> Optional[Type]:
+        """Return the most recently imported class that matches the given
+        xsi:type qname."""
+        types = self.find_types(qname)
+        return types[-1] if types else None
 
-        return None
+    def find_subclass(self, clazz: Type, qname: str) -> Optional[Type]:
+        """Compare all classes that match the given xsi:type qname and return
+        the first one that is either a subclass or shares the same parent class
+        as the original class."""
 
-    def match_class_source_qname(self, clazz: Type, xsi_type: str) -> bool:
-        """Match a given source qualified name with the given xsi type."""
-        if is_dataclass(clazz):
-            meta = self.build(clazz)
-            return meta.source_qname == xsi_type
+        types = self.find_types(qname)
+        if types:
+            for tp in types:
+                for tp_mro in tp.__mro__:
+                    if tp_mro is not object and tp_mro in clazz.__mro__:
+                        return tp
 
-        return False
+        return None
 
     def build(self, clazz: Type, parent_ns: Optional[str] = None) -> XmlMeta:
         """Fetch from cache or build the metadata object for the given class
@@ -199,6 +218,9 @@ class XmlContext:
             qname = build_qname(default_namespace, choice.get("name", "any"))
             nillable = choice.get("nillable", False)
 
+            if xml_type == XmlType.ELEMENT and len(types) == 1 and types[0] == object:
+                derived = True
+
             yield xml_clazz(
                 name=parent_name,
                 qname=qname,
@@ -329,6 +351,13 @@ class XmlContext:
 
         return XmlType.ELEMENT
 
+    @classmethod
+    def get_subclasses(cls, clazz: Type):
+        if clazz is not type:
+            for subclass in clazz.__subclasses__():
+                yield from cls.get_subclasses(subclass)
+                yield subclass
+
     def local_name(self, name: str, xml_type: Optional[str] = None) -> str:
         if xml_type == "Attribute":
             return self.attribute_name(name)
diff --git a/xsdata/formats/dataclass/models/elements.py b/xsdata/formats/dataclass/models/elements.py
index 01000f12..4197ee0b 100644
--- a/xsdata/formats/dataclass/models/elements.py
+++ b/xsdata/formats/dataclass/models/elements.py
@@ -201,10 +201,6 @@ class XmlWildcard(XmlVar):
     def is_wildcard(self) -> bool:
         return True
 
-    @property
-    def is_any_type(self) -> bool:
-        return True
-
     def matches(self, qname: str) -> bool:
         """Match the given qname to the wildcard allowed namespaces."""
 
@@ -304,6 +300,9 @@ class XmlMeta:
     def namespace(self) -> Optional[str]:
         return split_qname(self.qname)[0]
 
+    def has_var(self, qname: str = "*", mode: FindMode = FindMode.ALL) -> bool:
+        return self.find_var(qname, mode) is not None
+
     def find_var(
         self, qname: str = "*", mode: FindMode = FindMode.ALL
     ) -> Optional[XmlVar]:
diff --git a/xsdata/formats/dataclass/parsers/nodes.py b/xsdata/formats/dataclass/parsers/nodes.py
index 4216b6a5..b1691be3 100644
--- a/xsdata/formats/dataclass/parsers/nodes.py
+++ b/xsdata/formats/dataclass/parsers/nodes.py
@@ -132,26 +132,23 @@ class ElementNode(XmlNode):
             )
 
         if var.clazz:
-            xsi_type = ParserUtils.xsi_type(attrs, ns_map)
-            is_nillable = ParserUtils.is_nillable(attrs)
-            meta = self.context.fetch(var.clazz, self.meta.namespace, xsi_type)
-            mixed = self.meta.find_var(mode=FindMode.MIXED_CONTENT)
-
-            if not is_nillable and meta.nillable:
-                return None
-
-            return ElementNode(
-                meta=meta,
-                config=self.config,
-                attrs=attrs,
-                ns_map=ns_map,
-                context=self.context,
-                position=position,
-                derived=var.derived,
-                mixed=mixed is not None,
+            return self.build_element_node(
+                var.clazz, attrs, ns_map, position, var.derived
             )
 
         if var.is_any_type:
+            node = self.build_element_node(None, attrs, ns_map, position, var.derived)
+            if not node:
+                node = AnyTypeNode(
+                    var=var,
+                    attrs=attrs,
+                    ns_map=ns_map,
+                    position=position,
+                    mixed=self.meta.has_var(mode=FindMode.MIXED_CONTENT),
+                )
+            return node
+
+        if var.is_wildcard:
             return WildcardNode(var=var, attrs=attrs, ns_map=ns_map, position=position)
 
         return PrimitiveNode(var=var, ns_map=ns_map)
@@ -170,6 +167,88 @@ class ElementNode(XmlNode):
             else:
                 yield var
 
+    def build_element_node(
+        self,
+        clazz: Optional[Type],
+        attrs: Dict,
+        ns_map: Dict,
+        position: int,
+        derived: bool,
+    ) -> Optional[XmlNode]:
+        xsi_type = ParserUtils.xsi_type(attrs, ns_map)
+
+        if clazz is None:
+            if not xsi_type:
+                return None
+
+            clazz = self.context.find_type(xsi_type)
+            xsi_type = None
+
+        if clazz is None:
+            return None
+
+        is_nillable = ParserUtils.is_nillable(attrs)
+        meta = self.context.fetch(clazz, self.meta.namespace, xsi_type)
+
+        if not is_nillable and meta.nillable:
+            return None
+
+        return ElementNode(
+            meta=meta,
+            config=self.config,
+            attrs=attrs,
+            ns_map=ns_map,
+            context=self.context,
+            position=position,
+            derived=derived,
+            mixed=self.meta.has_var(mode=FindMode.MIXED_CONTENT),
+        )
+
+
+@dataclass
+class AnyTypeNode(XmlNode):
+
+    var: XmlVar
+    attrs: Dict
+    ns_map: Dict
+    position: int
+    mixed: bool = False
+    has_children: bool = field(init=False, default=False)
+
+    def child(self, qname: str, attrs: Dict, ns_map: Dict, position: int) -> "XmlNode":
+        self.has_children = True
+        return WildcardNode(position=position, var=self.var, attrs=attrs, ns_map=ns_map)
+
+    def bind(self, qname: str, text: NoneStr, tail: NoneStr, objects: List) -> bool:
+        obj: Any = None
+        if self.has_children:
+            obj = AnyElement(
+                qname=qname,
+                text=ParserUtils.normalize_content(text),
+                tail=ParserUtils.normalize_content(tail),
+                ns_map=self.ns_map,
+                attributes=ParserUtils.parse_any_attributes(self.attrs, self.ns_map),
+                children=ParserUtils.fetch_any_children(self.position, objects),
+            )
+            objects.append((self.var.qname, obj))
+        else:
+            var = self.var
+            ns_map = self.ns_map
+            datatype = ParserUtils.data_type(self.attrs, self.ns_map)
+            obj = ParserUtils.parse_value(text, [datatype.local], var.default, ns_map)
+
+            if var.derived:
+                obj = DerivedElement(qname=qname, value=obj)
+
+            objects.append((qname, obj))
+
+            if self.mixed:
+                tail = ParserUtils.normalize_content(tail)
+                if tail:
+                    objects.append((None, tail))
+
+        return True
+
 
 @dataclass
 class WildcardNode(XmlNode):
diff --git a/xsdata/formats/dataclass/parsers/utils.py b/xsdata/formats/dataclass/parsers/utils.py
index 2192aa45..6bf8b42b 100644
--- a/xsdata/formats/dataclass/parsers/utils.py
+++ b/xsdata/formats/dataclass/parsers/utils.py
@@ -4,15 +4,16 @@ from typing import Dict
 from typing import List
 from typing import Optional
 from typing import Type
-from xml.etree.ElementTree import QName
 
 from xsdata.formats.converter import converter
+from xsdata.formats.converter import QNameConverter
 from xsdata.formats.dataclass.models.elements import FindMode
 from xsdata.formats.dataclass.models.elements import XmlMeta
 from xsdata.formats.dataclass.models.elements import XmlVar
 from xsdata.formats.dataclass.models.generics import AnyElement
 from xsdata.formats.dataclass.models.generics import DerivedElement
 from xsdata.logger import logger
+from xsdata.models.enums import DataType
 from xsdata.models.enums import QNames
 from xsdata.utils import text
 from xsdata.utils.namespaces import build_qname
@@ -21,12 +22,24 @@ from xsdata.utils.namespaces import build_qname
 class ParserUtils:
     @classmethod
     def xsi_type(cls, attrs: Dict, ns_map: Dict) -> Optional[str]:
-        """Parse the elements xsi:type attribute if present."""
+        """Parse the xsi:type attribute if present."""
         xsi_type = attrs.get(QNames.XSI_TYPE)
+        if not xsi_type:
+            return None
+
+        namespace, name = QNameConverter.resolve(xsi_type, ns_map)
+        return build_qname(namespace, name)
+
+    @classmethod
+    def data_type(cls, attrs: Dict, ns_map: Dict) -> DataType:
+        """Convert the xsi:type attribute to a DataType, defaults to
+        DataType.STRING."""
+        xsi_type = cls.xsi_type(attrs, ns_map)
+        datatype = DataType.STRING
         if xsi_type:
-            return cls.parse_value(xsi_type, [QName], None, ns_map).text
+            datatype = DataType.from_qname(xsi_type) or datatype
 
-        return None
+        return datatype
 
     @classmethod
     def is_nillable(cls, attrs: Dict) -> bool:
@@ -86,7 +99,10 @@ class ParserUtils:
             if value is None:
                 value = ""
 
-            params[var.name].append(cls.prepare_generic_value(qname, value))
+            if qname:
+                value = cls.prepare_generic_value(qname, value)
+
+            params[var.name].append(value)
 
     @classmethod
     def bind_var(cls, params: Dict, var: XmlVar, value: Any) -> bool:
diff --git a/xsdata/formats/dataclass/serializers/xml.py b/xsdata/formats/dataclass/serializers/xml.py
index 79f7b4b0..1c14e6cf 100644
--- a/xsdata/formats/dataclass/serializers/xml.py
+++ b/xsdata/formats/dataclass/serializers/xml.py
@@ -21,12 +21,12 @@ from xsdata.formats.dataclass.models.generics import DerivedElement
 from xsdata.formats.dataclass.serializers.mixins import XmlWriter
 from xsdata.formats.dataclass.serializers.mixins import XmlWriterEvent
 from xsdata.formats.dataclass.serializers.writers import LxmlEventWriter
+from xsdata.models.enums import DataType
 from xsdata.models.enums import QNames
 from xsdata.utils import namespaces
 from xsdata.utils.constants import EMPTY_MAP
 from xsdata.utils.namespaces import split_qname
 
-
 NoneStr = Optional[str]
 
 
@@ -114,12 +114,16 @@ class XmlSerializer(AbstractSerializer):
         yield XmlWriterEvent.END, qname
 
     def write_xsi_type(self, value: Any, var: XmlVar, namespace: NoneStr) -> Generator:
-        """Produce an events stream from a dataclass with xsi abstract type
-        check."""
-        xsi_type = self.xsi_type(var, value, namespace)
-        yield from self.write_dataclass(
-            value, namespace, var.qname, var.nillable, xsi_type
-        )
+        """Produce an events stream from a dataclass for the given var with
+        with xsi abstract type check for non wildcards."""
+
+        if var.is_wildcard:
+            yield from self.write_dataclass(value, namespace)
+        else:
+            xsi_type = self.xsi_type(var, value, namespace)
+            yield from self.write_dataclass(
+                value, namespace, var.qname, var.nillable, xsi_type
+            )
 
     def write_value(self, value: Any, var: XmlVar, namespace: NoneStr) -> Generator:
         """
@@ -139,7 +143,7 @@ class XmlSerializer(AbstractSerializer):
             yield from self.write_elements(value, var, namespace)
         elif var.is_list and isinstance(value, list):
             yield from self.write_list(value, var, namespace)
-        elif var.is_any_type:
+        elif var.is_any_type or var.is_wildcard:
             yield from self.write_any_type(value, var, namespace)
         elif var.dataclass:
             yield from self.write_xsi_type(value, var, namespace)
@@ -178,17 +182,16 @@ class XmlSerializer(AbstractSerializer):
         The object can be a dataclass or a generic object or any other
         simple type.
         """
-        if not is_dataclass(value):
-            if var.is_element:
-                yield from self.write_element(value, var, namespace)
-            else:
-                yield from self.write_data(value, var, namespace)
-        elif isinstance(value, AnyElement):
+        if isinstance(value, AnyElement):
             yield from self.write_wildcard(value, var, namespace)
         elif isinstance(value, DerivedElement):
             yield from self.write_dataclass(value.value, namespace, qname=value.qname)
+        elif is_dataclass(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_dataclass(value, namespace)
+            yield from self.write_data(value, var, namespace)
 
     def write_wildcard(
         self, value: AnyElement, var: XmlVar, namespace: NoneStr
@@ -209,17 +212,16 @@ class XmlSerializer(AbstractSerializer):
         if value.qname:
             yield XmlWriterEvent.END, value.qname
 
-        yield XmlWriterEvent.DATA, value.tail
+        if value.tail:
+            yield XmlWriterEvent.DATA, value.tail
 
     def xsi_type(self, var: XmlVar, value: Any, namespace: NoneStr) -> Optional[QName]:
         """Get xsi:type if the given value is a derived instance."""
-        if not value or value.__class__ in var.types or not var.dataclass:
+        if not value or value.__class__ in var.types:
             return None
 
         clazz = var.clazz
-        assert clazz is not None  # not possible :)
-
-        if self.context.is_derived(value, clazz):
+        if clazz is None or self.context.is_derived(value, clazz):
             meta = self.context.fetch(value.__class__, namespace)
             return QName(meta.source_qname)
 
@@ -269,6 +271,11 @@ class XmlSerializer(AbstractSerializer):
         if var.nillable:
             yield XmlWriterEvent.ATTR, QNames.XSI_NIL, "true"
 
+        if value is not None and var.is_any_type:
+            datatype = DataType.from_value(value)
+            if datatype != DataType.STRING:
+                yield XmlWriterEvent.ATTR, QNames.XSI_TYPE, datatype.qname
+
         yield XmlWriterEvent.DATA, value
         yield XmlWriterEvent.END, var.qname
 
diff --git a/xsdata/models/enums.py b/xsdata/models/enums.py
index f4d45ee9..78e33cf4 100644
--- a/xsdata/models/enums.py
+++ b/xsdata/models/enums.py
@@ -1,6 +1,7 @@
 from decimal import Decimal
 from enum import Enum
 from pathlib import Path
+from typing import Any
 from typing import Iterator
 from typing import Optional
 from xml.etree.ElementTree import QName
@@ -148,10 +149,30 @@ class DataType(Enum):
 
     @classmethod
     def get_enum(cls, code: str) -> Optional["DataType"]:
-        return __XSDType__.get(code) if code else None
+        return __DataTypeCodeIndex__.get(code) if code else None
+
+    @classmethod
+    def from_value(cls, value: Any) -> "DataType":
+        if isinstance(value, bool):
+            return DataType.BOOLEAN
+        if isinstance(value, int):
+            return DataType.INT
+        if isinstance(value, float):
+            return DataType.FLOAT
+        if isinstance(value, Decimal):
+            return DataType.DECIMAL
+        if isinstance(value, QName):
+            return DataType.QNAME
+
+        return DataType.STRING
+
+    @classmethod
+    def from_qname(cls, qname: str) -> Optional["DataType"]:
+        return __DataTypeQNameIndex__.get(qname)
 
 
-__XSDType__ = {xsd.code: xsd for xsd in DataType}
+__DataTypeCodeIndex__ = {xsd.code: xsd for xsd in DataType}
+__DataTypeQNameIndex__ = {xsd.qname: xsd for xsd in DataType}
 
 
 class EventType:


Test patch:
diff --git a/tests/formats/dataclass/parsers/test_nodes.py b/tests/formats/dataclass/parsers/test_nodes.py
index e74f0c7a..4eaedee6 100644
--- a/tests/formats/dataclass/parsers/test_nodes.py
+++ b/tests/formats/dataclass/parsers/test_nodes.py
@@ -23,6 +23,7 @@ from xsdata.formats.dataclass.models.generics import AnyElement
 from xsdata.formats.dataclass.models.generics import DerivedElement
 from xsdata.formats.dataclass.parsers.config import ParserConfig
 from xsdata.formats.dataclass.parsers.mixins import XmlHandler
+from xsdata.formats.dataclass.parsers.nodes import AnyTypeNode
 from xsdata.formats.dataclass.parsers.nodes import ElementNode
 from xsdata.formats.dataclass.parsers.nodes import NodeParser
 from xsdata.formats.dataclass.parsers.nodes import PrimitiveNode
@@ -30,6 +31,8 @@ from xsdata.formats.dataclass.parsers.nodes import SkipNode
 from xsdata.formats.dataclass.parsers.nodes import UnionNode
 from xsdata.formats.dataclass.parsers.nodes import WildcardNode
 from xsdata.formats.dataclass.parsers.utils import ParserUtils
+from xsdata.models.enums import DataType
+from xsdata.models.enums import Namespace
 from xsdata.models.enums import QNames
 from xsdata.models.mixins import attribute
 from xsdata.models.mixins import element
@@ -355,7 +358,43 @@ class ElementNodeTests(TestCase):
         attrs = {QNames.XSI_NIL: "false"}
         self.assertIsNone(self.node.build_node(var, attrs, ns_map, 10))
 
-    def test_build_node_with_any_type_var(self):
+    def test_build_node_with_any_type_var_with_matching_xsi_type(self):
+        var = XmlElement(name="a", qname="a", types=[object])
+
+        actual = self.node.build_node(var, {QNames.XSI_TYPE: "Foo"}, {}, 10)
+
+        self.assertIsInstance(actual, ElementNode)
+        self.assertEqual(10, actual.position)
+        self.assertEqual(self.context.build(Foo), actual.meta)
+        self.assertEqual({QNames.XSI_TYPE: "Foo"}, actual.attrs)
+        self.assertEqual({}, actual.ns_map)
+        self.assertFalse(actual.mixed)
+
+    def test_build_node_with_any_type_var_with_no_matching_xsi_type(self):
+        var = XmlElement(name="a", qname="a", types=[object])
+        attrs = {QNames.XSI_TYPE: "noMatch"}
+        actual = self.node.build_node(var, attrs, {}, 10)
+
+        self.assertIsInstance(actual, AnyTypeNode)
+        self.assertEqual(10, actual.position)
+        self.assertEqual(var, actual.var)
+        self.assertEqual(attrs, actual.attrs)
+        self.assertEqual({}, actual.ns_map)
+        self.assertFalse(actual.mixed)
+
+    def test_build_node_with_any_type_var_with_no_xsi_type(self):
+        var = XmlElement(name="a", qname="a", types=[object])
+        attrs = {}
+        actual = self.node.build_node(var, attrs, {}, 10)
+
+        self.assertIsInstance(actual, AnyTypeNode)
+        self.assertEqual(10, actual.position)
+        self.assertEqual(var, actual.var)
+        self.assertEqual(attrs, actual.attrs)
+        self.assertEqual({}, actual.ns_map)
+        self.assertFalse(actual.mixed)
+
+    def test_build_node_with_wildcard_var(self):
         var = XmlWildcard(name="a", qname="a", types=[], dataclass=False)
 
         actual = self.node.build_node(var, {}, {}, 10)
@@ -375,6 +414,81 @@ class ElementNodeTests(TestCase):
         self.assertEqual(ns_map, actual.ns_map)
 
 
+class AnyTypeNodeTests(TestCase):
+    def setUp(self) -> None:
+        self.var = XmlElement(name="a", qname="a", types=[object])
+        self.node = AnyTypeNode(position=0, var=self.var, attrs={}, ns_map={})
+
+    def test_child(self):
+        self.assertFalse(self.node.has_children)
+
+        attrs = {"a": 1}
+        ns_map = {"ns0": "b"}
+        actual = self.node.child("foo", attrs, ns_map, 10)
+
+        self.assertIsInstance(actual, WildcardNode)
+        self.assertEqual(10, actual.position)
+        self.assertEqual(self.var, actual.var)
+        self.assertEqual(attrs, actual.attrs)
+        self.assertEqual(ns_map, actual.ns_map)
+        self.assertTrue(self.node.has_children)
+
+    def test_bind_with_children(self):
+        text = "\n "
+        tail = "bar"
+        generic = AnyElement(
+            qname="a",
+            text=None,
+            tail="bar",
+            ns_map={},
+            attributes={},
+            children=[1, 2, 3],
+        )
+
+        objects = [("a", 1), ("b", 2), ("c", 3)]
+
+        self.node.has_children = True
+        self.assertTrue(self.node.bind("a", text, tail, objects))
+        self.assertEqual(self.var.qname, objects[-1][0])
+        self.assertEqual(generic, objects[-1][1])
+
+    def test_bind_with_simple_type(self):
+        objects = []
+
+        self.node.attrs[QNames.XSI_TYPE] = "xs:float"
+        self.node.ns_map["xs"] = Namespace.XS.uri
+
+        self.assertTrue(self.node.bind("a", "10", None, objects))
+        self.assertEqual(self.var.qname, objects[-1][0])
+        self.assertEqual(10.0, objects[-1][1])
+
+    def test_bind_with_simple_type_derived(self):
+        objects = []
+
+        self.node.var = XmlElement(name="a", qname="a", types=[object], derived=True)
+        self.node.attrs[QNames.XSI_TYPE] = DataType.FLOAT.qname
+
+        self.assertTrue(self.node.bind("a", "10", None, objects))
+        self.assertEqual(self.var.qname, objects[-1][0])
+        self.assertEqual(DerivedElement(qname="a", value=10.0), objects[-1][1])
+
+    def test_bind_with_simple_type_with_mixed_content(self):
+        objects = []
+
+        self.node.mixed = True
+        self.node.attrs[QNames.XSI_TYPE] = DataType.FLOAT.qname
+
+        self.assertTrue(self.node.bind("a", "10", "pieces", objects))
+        self.assertEqual(self.var.qname, objects[-2][0])
+        self.assertEqual(10.0, objects[-2][1])
+        self.assertIsNone(objects[-1][0])
+        self.assertEqual("pieces", objects[-1][1])
+
+        self.assertTrue(self.node.bind("a", "10", "\n", objects))
+        self.assertEqual(self.var.qname, objects[-1][0])
+        self.assertEqual(10.0, objects[-1][1])
+
+
 class WildcardNodeTests(TestCase):
     def test_bind(self):
         text = "\n "
diff --git a/tests/formats/dataclass/parsers/test_utils.py b/tests/formats/dataclass/parsers/test_utils.py
index c5d85855..e9229219 100644
--- a/tests/formats/dataclass/parsers/test_utils.py
+++ b/tests/formats/dataclass/parsers/test_utils.py
@@ -18,6 +18,7 @@ from xsdata.formats.dataclass.models.elements import XmlWildcard
 from xsdata.formats.dataclass.models.generics import AnyElement
 from xsdata.formats.dataclass.models.generics import DerivedElement
 from xsdata.formats.dataclass.parsers.utils import ParserUtils
+from xsdata.models.enums import DataType
 from xsdata.models.enums import Namespace
 from xsdata.models.enums import QNames
 
@@ -37,6 +38,18 @@ class ParserUtilsTests(TestCase):
         attrs = {QNames.XSI_TYPE: "bar:foo"}
         self.assertEqual("{xsdata}foo", ParserUtils.xsi_type(attrs, ns_map))
 
+    def test_data_type(self):
+        ns_map = {"bar": "xsdata"}
+        attrs = {}
+        self.assertEqual(DataType.STRING, ParserUtils.data_type(attrs, ns_map))
+
+        ns_map = {"xs": Namespace.XS.uri}
+        attrs = {QNames.XSI_TYPE: "xs:foo"}
+        self.assertEqual(DataType.STRING, ParserUtils.data_type(attrs, ns_map))
+
+        attrs = {QNames.XSI_TYPE: "xs:float"}
+        self.assertEqual(DataType.FLOAT, ParserUtils.data_type(attrs, ns_map))
+
     @mock.patch.object(ConverterAdapter, "from_string", return_value=2)
     def test_parse_value(self, mock_from_string):
         self.assertEqual(1, ParserUtils.parse_value(None, [int], 1))
@@ -122,13 +135,14 @@ class ParserUtilsTests(TestCase):
             ("b", None),
             ("d", data_class),
             ("foo", generic),
+            (None, "foo"),
         ]
 
         var = XmlWildcard(name="foo", qname="{any}foo")
         params = {}
         ParserUtils.bind_mixed_objects(params, var, 1, objects)
 
-        expected = {var.name: [AnyElement(qname="b", text=""), derived, generic]}
+        expected = {var.name: [AnyElement(qname="b", text=""), derived, generic, "foo"]}
         self.assertEqual(expected, params)
 
     def test_fetch_any_children(self):
diff --git a/tests/formats/dataclass/serializers/test_xml.py b/tests/formats/dataclass/serializers/test_xml.py
index c36898fc..30c95ff3 100644
--- a/tests/formats/dataclass/serializers/test_xml.py
+++ b/tests/formats/dataclass/serializers/test_xml.py
@@ -20,6 +20,7 @@ from xsdata.formats.dataclass.models.generics import AnyElement
 from xsdata.formats.dataclass.models.generics import DerivedElement
 from xsdata.formats.dataclass.serializers import XmlSerializer
 from xsdata.formats.dataclass.serializers.mixins import XmlWriterEvent
+from xsdata.models.enums import DataType
 from xsdata.models.enums import QNames
 
 
@@ -117,7 +118,6 @@ class XmlSerializerTests(TestCase):
             (XmlWriterEvent.START, "br"),
             (XmlWriterEvent.DATA, None),
             (XmlWriterEvent.END, "br"),
-            (XmlWriterEvent.DATA, None),
             (XmlWriterEvent.START, "{xsdata}BookForm"),
             (XmlWriterEvent.ATTR, "id", "123"),
             (XmlWriterEvent.ATTR, "lang", "en"),
@@ -202,7 +202,6 @@ class XmlSerializerTests(TestCase):
             (XmlWriterEvent.ATTR, "e", 2),
             (XmlWriterEvent.DATA, "b"),
             (XmlWriterEvent.DATA, "g"),
-            (XmlWriterEvent.DATA, None),
             (XmlWriterEvent.DATA, "h"),
             (XmlWriterEvent.END, "a"),
             (XmlWriterEvent.DATA, "c"),
@@ -279,6 +278,31 @@ class XmlSerializerTests(TestCase):
         self.assertIsInstance(result, Generator)
         self.assertEqual(expected, list(result))
 
+    def test_write_element_with_any_type_var(self):
+        var = XmlElement(qname="a", name="a", types=[object])
+        expected = [
+            (XmlWriterEvent.START, "a"),
+            (XmlWriterEvent.ATTR, QNames.XSI_TYPE, DataType.INT.qname),
+            (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 = XmlElement(qname="a", name="a", types=[object])
+        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 = XmlElements(
             name="compound",
@@ -300,7 +324,7 @@ class XmlSerializerTests(TestCase):
         var = XmlElements(
             name="compound",
             qname="compound",
-            choices=[XmlElement(qname="a", name="a", types=[int])],
+            choices=[XmlElement(qname="a", name="a", types=[A])],
         )
         value = DerivedElement(qname="a", value=A("foo"))
         expected = [
@@ -324,7 +348,6 @@ class XmlSerializerTests(TestCase):
             (XmlWriterEvent.START, "a"),
             (XmlWriterEvent.DATA, "1"),
             (XmlWriterEvent.END, "a"),
-            (XmlWriterEvent.DATA, None),
         ]
 
         result = self.serializer.write_value(value, var, "xsdata")
diff --git a/tests/formats/dataclass/test_context.py b/tests/formats/dataclass/test_context.py
index 166149b0..2b901ab1 100644
--- a/tests/formats/dataclass/test_context.py
+++ b/tests/formats/dataclass/test_context.py
@@ -14,6 +14,7 @@ from unittest import TestCase
 
 from tests.fixtures.books import BookForm
 from tests.fixtures.books import Books
+from tests.fixtures.books import BooksForm
 from tests.fixtures.defxmlschema.chapter03prod import Product
 from tests.fixtures.defxmlschema.chapter05prod import ProductType
 from tests.fixtures.defxmlschema.chapter13 import ItemsType
@@ -26,6 +27,7 @@ from xsdata.formats.dataclass.models.elements import XmlElement
 from xsdata.formats.dataclass.models.elements import XmlElements
 from xsdata.formats.dataclass.models.elements import XmlMeta
 from xsdata.formats.dataclass.models.elements import XmlWildcard
+from xsdata.models.enums import DataType
 from xsdata.utils import text
 from xsdata.utils.namespaces import build_qname
 
@@ -87,30 +89,25 @@ class XmlContextTests(TestCase):
         self.assertEqual(xsi_meta, actual)
         mock_find_subclass.assert_called_once_with(ItemsType, "foo")
 
+    def test_find(self):
+        self.assertIsNone(self.ctx.find_type(DataType.FLOAT.qname))
+        self.assertEqual(BookForm, self.ctx.find_type("{urn:books}BookForm"))
+
+        self.ctx.xsi_cache["{urn:books}BookForm"].append(BooksForm)
+        self.assertEqual(BooksForm, self.ctx.find_type("{urn:books}BookForm"))
+
     def test_find_subclass(self):
         a = make_dataclass("A", fields=[])
         b = make_dataclass("B", fields=[], bases=(a,))
         c = make_dataclass("C", fields=[], bases=(a,))
+        other = make_dataclass("Other", fields=[])
 
         self.assertEqual(b, self.ctx.find_subclass(a, "B"))
         self.assertEqual(b, self.ctx.find_subclass(c, "B"))
         self.assertEqual(a, self.ctx.find_subclass(b, "A"))
         self.assertEqual(a, self.ctx.find_subclass(c, "A"))
-        self.assertIsNone(self.ctx.find_subclass(c, "What"))
-
-    def test_match_class_name(self):
-        # no meta name
-        self.assertFalse(self.ctx.match_class_source_qname(ItemsType, "qname_foo"))
-        self.assertTrue(self.ctx.match_class_source_qname(ItemsType, "ItemsType"))
-
-        # with meta name
-        product_qname = "{http://example.org/prod}product"
-        self.assertFalse(self.ctx.match_class_source_qname(Product, "ItemsType"))
-        self.assertTrue(self.ctx.match_class_source_qname(Product, product_qname))
-
-        # not dataclass
-        self.assertFalse(self.ctx.match_class_source_qname(object, "object"))
-        self.assertFalse(self.ctx.match_class_source_qname(int, "int"))
+        self.assertIsNone(self.ctx.find_subclass(c, "Unknown"))
+        self.assertIsNone(self.ctx.find_subclass(c, "Other"))
 
     @mock.patch.object(XmlContext, "get_type_hints")
     def test_build_build_vars(self, mock_get_type_hints):
@@ -331,12 +328,20 @@ class XmlContextTests(TestCase):
                     namespaces=["bar"],
                     derived=True,
                 ),
+                XmlElement(
+                    name="compound",
+                    qname="{bar}o",
+                    nillable=False,
+                    types=[object],
+                    namespaces=["bar"],
+                    derived=True,
+                ),
                 XmlWildcard(
                     name="compound",
                     qname="{http://www.w3.org/1999/xhtml}any",
                     types=[object],
                     namespaces=["http://www.w3.org/1999/xhtml"],
-                    derived=False,
+                    derived=True,
                 ),
             ],
             types=[object],
@@ -481,6 +486,7 @@ class Node:
                 {"name": "x", "type": List[str], "tokens": True},
                 {"name": "y", "type": List[int], "nillable": True},
                 {"name": "z", "type": List[int]},
+                {"name": "o", "type": object},
                 {
                     "wildcard": True,
                     "type": object,
diff --git a/tests/formats/dataclass/test_elements.py b/tests/formats/dataclass/test_elements.py
index 42ce2aaf..2525f1d3 100644
--- a/tests/formats/dataclass/test_elements.py
+++ b/tests/formats/dataclass/test_elements.py
@@ -142,10 +142,6 @@ class XmlWildcardTests(TestCase):
         self.assertIsInstance(var, XmlVar)
         self.assertTrue(var.is_wildcard)
 
-    def test_property_is_any_type(self):
-        var = XmlWildcard(name="foo", qname="foo")
-        self.assertTrue(var.is_any_type)
-
     def test_matches(self):
         var = XmlWildcard(name="foo", qname="foo")
         self.assertTrue(var.matches("*"))
diff --git a/tests/models/enums/__init__.py b/tests/models/enums/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/models/enums/test_datatype.py b/tests/models/enums/test_datatype.py
new file mode 100644
index 00000000..ac86b9e3
--- /dev/null
+++ b/tests/models/enums/test_datatype.py
@@ -0,0 +1,15 @@
+from decimal import Decimal
+from unittest import TestCase
+from xml.etree.ElementTree import QName
+
+from xsdata.models.enums import DataType
+
+
+class DataTypeTests(TestCase):
+    def test_from_value(self):
+        self.assertEqual(DataType.BOOLEAN, DataType.from_value(True))
+        self.assertEqual(DataType.INT, DataType.from_value(1))
+        self.assertEqual(DataType.FLOAT, DataType.from_value(1.1))
+        self.assertEqual(DataType.DECIMAL, DataType.from_value(Decimal(1.1)))
+        self.assertEqual(DataType.QNAME, DataType.from_value(QName("a")))
+        self.assertEqual(DataType.STRING, DataType.from_value("a"))