Repository: tefra/xsdata

Issue:
Mixed complex types generates wrong XML tag names for children elements
Given the following XSD : 

```xsd
<?xml version="1.0"?>
<xs:schema
  xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://example.com"
  xmlns="http://example.com" elementFormDefault="qualified">
  <xs:element name='class' type="foo.class" />
  <xs:complexType name="foo.class" mixed="true">
    <xs:sequence>
      <xs:choice minOccurs="0" maxOccurs="unbounded">
        <xs:element name="teacher" type="foo.teacher"/>
        <xs:element name="classroom" type="foo.classroom"/>
      </xs:choice>
      <xs:element name='student' type='foo.student' minOccurs='0' maxOccurs='unbounded' />
    </xs:sequence>
  </xs:complexType>
  <xs:complexType name="foo.student">
    <xs:sequence>
      <xs:element name="firstname" type="xs:string"/>
      <xs:element name="lastname" type="xs:string"/>
      <xs:element name="nickname" type="xs:string"/>
      <xs:element name="marks" type="xs:positiveInteger"/>
    </xs:sequence>
    <xs:attribute name='rollno' type='xs:positiveInteger'/>
  </xs:complexType>
  <xs:complexType name="foo.teacher">
    <xs:attribute name="firstname" type="xs:string"/>
    <xs:attribute name="lastname" type="xs:string"/>
  </xs:complexType>
  <xs:complexType name="foo.classroom">
    <xs:attribute name="name" type="xs:string"/>
    <xs:attribute name="floor" type="xs:string"/>
  </xs:complexType>
</xs:schema>
```

I'm generating a Serializer using 

```shell
xsdata generate --config .xsdata.xml foo.xsd
```

Config : 

```xml
<?xml version="1.0" encoding="UTF-8"?>
<Config
  xmlns="http://pypi.org/project/xsdata" version="22.5">
  <Output maxLineLength="79">
    <Package>foo</Package>
    <Format repr="true" eq="true" order="false" unsafeHash="false" frozen="false" slots="false" kwOnly="false">dataclasses</Format>
    <Structure>namespace-clusters</Structure>
    <DocstringStyle>reStructuredText</DocstringStyle>
    <RelativeImports>false</RelativeImports>
    <CompoundFields defaultName="choice" forceDefaultName="false">false</CompoundFields>
    <PostponedAnnotations>false</PostponedAnnotations>
    <UnnestClasses>false</UnnestClasses>
  </Output>
  <Conventions>
    <ClassName case="pascalCase" safePrefix="type"/>
    <FieldName case="snakeCase" safePrefix="value"/>
    <ConstantName case="screamingSnakeCase" safePrefix="value"/>
    <ModuleName case="snakeCase" safePrefix="mod"/>
    <PackageName case="snakeCase" safePrefix="pkg"/>
  </Conventions>
  <Substitutions></Substitutions>
</Config>
```

And using the following code to generate a custom XML : 

```python
from xsdata.formats.dataclass.context import XmlContext
from xsdata.formats.dataclass.parsers import XmlParser
from xsdata.formats.dataclass.serializers import XmlSerializer
from xsdata.formats.dataclass.serializers.config import SerializerConfig

from foo.com.example import *

context = XmlContext()
parser = XmlParser(context=context)

config = SerializerConfig(pretty_print=True, ignore_default_attributes=True)
serializer = XmlSerializer(context=context, config=config)

klass = Class(
    content=[
        FooStudent(firstname="John", lastname="Doe"),
        FooStudent(firstname="Jane", lastname="Doe"),
        FooTeacher(firstname="George", lastname="Orwell"),
    ]
)

print(
    serializer.render(
        klass,
        ns_map={
            None: "http://example.com",
            "xsi": "http://www.w3.org/2001/XMLSchema-instance",
        },
    )
)
```

Which gives : 

```xml
<?xml version="1.0" encoding="UTF-8"?>
<class xmlns="http://example.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <foo.student>
    <firstname>John</firstname>
    <lastname>Doe</lastname>
  </foo.student>
  <foo.student>
    <firstname>Jane</firstname>
    <lastname>Doe</lastname>
  </foo.student>
  <foo.teacher firstname="George" lastname="Orwell"/>
</class>
```

I'm bothered by the `<foo.student>` and `<foo.teacher>` tag names. Why are children elements being named according to the name of their types (`FooStudent` and `FooName`), instead of the `name` attribute of the `<xs:element>` as defined by the `foo.class` complexType (i.e. `<student>` and `<teacher>`) ?

I'd expect the resulting XML to be :  

```xml
<?xml version="1.0" encoding="UTF-8"?>
<class xmlns="http://example.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <student>
    <firstname>John</firstname>
    <lastname>Doe</lastname>
  </student>
  <student>
    <firstname>Jane</firstname>
    <lastname>Doe</lastname>
  </student>
  <teacher firstname="George" lastname="Orwell"/>
</class>
```

I'm very much out of my depth here, so maybe that's a feature and not a bug, but any help would be appreciated !

Thanks a lot for this library, it's been super helpful and easy to use so far !

Discussion:
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=696)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=696&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=696&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=696&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=696&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=696&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=696&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=696&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=696&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=696&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=696&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=696&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=696&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=696) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=696&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=696&metric=new_duplicated_lines_density&view=list)


codecov[bot]: # [Codecov](https://codecov.io/gh/tefra/xsdata/pull/696?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) Report
> Merging [#696](https://codecov.io/gh/tefra/xsdata/pull/696?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) (3289668) into [master](https://codecov.io/gh/tefra/xsdata/commit/9f646daf8322c29b9da10efffdd01f7b5c528d07?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) (9f646da) will **decrease** coverage by `0.03%`.
> The diff coverage is `100.00%`.

```diff
@@             Coverage Diff             @@
##            master     #696      +/-   ##
===========================================
- Coverage   100.00%   99.96%   -0.04%     
===========================================
  Files           99       99              
  Lines         8688     8691       +3     
  Branches      2194     1661     -533     
===========================================
  Hits          8688     8688              
- Misses           0        2       +2     
- Partials         0        1       +1     
```


| [Impacted Files](https://codecov.io/gh/tefra/xsdata/pull/696?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) | Coverage Δ | |
|---|---|---|
| [xsdata/formats/dataclass/parsers/nodes/element.py](https://codecov.io/gh/tefra/xsdata/pull/696/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3BhcnNlcnMvbm9kZXMvZWxlbWVudC5weQ==) | `100.00% <100.00%> (ø)` | |
| [xsdata/formats/dataclass/serializers/xml.py](https://codecov.io/gh/tefra/xsdata/pull/696/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3NlcmlhbGl6ZXJzL3htbC5weQ==) | `100.00% <100.00%> (ø)` | |
| [xsdata/models/config.py](https://codecov.io/gh/tefra/xsdata/pull/696/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL21vZGVscy9jb25maWcucHk=) | `98.31% <0.00%> (-1.69%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou)

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=696)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=696&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=696&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=696&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=696&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=696&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=696&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=696&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=696&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=696&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=696&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=696&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=696&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=696) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=696&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=696&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/docs/examples.rst b/docs/examples.rst
index 86c4102f..c03d3b0b 100644
--- a/docs/examples.rst
+++ b/docs/examples.rst
@@ -11,12 +11,21 @@ Code Generation
     examples/docstrings
     examples/xml-modeling
     examples/json-modeling
-    examples/pycode-serializer
     examples/dtd-modeling
     examples/compound-fields
     examples/dataclasses-features
 
 
+Data Binding
+============
+
+.. toctree::
+    :maxdepth: 1
+
+    examples/pycode-serializer
+    examples/working-with-wildcards
+
+
 Advance Topics
 ==============
 
diff --git a/docs/faq/how-to-work-with-wildcard-fields.rst b/docs/examples/working-with-wildcards.rst
similarity index 65%
rename from docs/faq/how-to-work-with-wildcard-fields.rst
rename to docs/examples/working-with-wildcards.rst
index a7963af6..ac3c7584 100644
--- a/docs/faq/how-to-work-with-wildcard-fields.rst
+++ b/docs/examples/working-with-wildcards.rst
@@ -1,5 +1,7 @@
-How to work with wildcard fields?
-==================================
+======================
+Working with wildcards
+======================
+
 
 One of the xml schema traits is to support any extensions with wildcards.
 
@@ -39,6 +41,9 @@ The generator will roughly create this class for you.
     ...      )
 
 
+Generics
+========
+
 xsdata comes with two generic models that are used during parsing and you can also use
 to generate any custom xml element.
 
@@ -87,3 +92,63 @@ to generate any custom xml element.
       </bar>
     </MetadataType>
     <BLANKLINE>
+
+
+Mixed content
+=============
+
+For mixed content with known choices you can skip wrapping your instances with a
+generic model. During data binding xsdata will try first to match one of the qualified
+choices.
+
+.. doctest::
+
+    >>> @dataclass
+    ... class Beta:
+    ...     class Meta:
+    ...         name = "beta"
+    ...
+    >>> @dataclass
+    ... class Alpha:
+    ...     class Meta:
+    ...         name = "alpha"
+    ...
+    >>> @dataclass
+    ... class Doc:
+    ...     class Meta:
+    ...         name = "doc"
+    ...
+    ...     content: List[object] = field(
+    ...         default_factory=list,
+    ...         metadata={
+    ...             "type": "Wildcard",
+    ...             "namespace": "##any",
+    ...             "mixed": True,
+    ...             "choices": (
+    ...                 {
+    ...                     "name": "a",
+    ...                     "type": Alpha,
+    ...                     "namespace": "",
+    ...                 },
+    ...                 {
+    ...                     "name": "b",
+    ...                     "type": Beta,
+    ...                     "namespace": "",
+    ...                 },
+    ...             ),
+    ...         }
+    ...     )
+    ...
+    >>> obj = Doc(
+    ...     content=[
+    ...         Alpha(),
+    ...         Beta(),
+    ...     ]
+    ... )
+    ...
+    >>> print(serializer.render(obj))
+    <doc>
+      <a/>
+      <b/>
+    </doc>
+    <BLANKLINE>
diff --git a/xsdata/formats/dataclass/parsers/nodes/element.py b/xsdata/formats/dataclass/parsers/nodes/element.py
index 6f891588..68c9b134 100644
--- a/xsdata/formats/dataclass/parsers/nodes/element.py
+++ b/xsdata/formats/dataclass/parsers/nodes/element.py
@@ -214,7 +214,7 @@ class ElementNode(XmlNode):
         generic instance add the current value as a child object.
         """
 
-        value = self.prepare_generic_value(qname, value)
+        value = self.prepare_generic_value(qname, value, var)
 
         if var.list_element:
             items = params.get(var.name)
@@ -240,11 +240,14 @@ class ElementNode(XmlNode):
 
         pos = self.position
         params[var.name] = [
-            self.prepare_generic_value(qname, value) for qname, value in objects[pos:]
+            self.prepare_generic_value(qname, value, var)
+            for qname, value in objects[pos:]
         ]
         del objects[pos:]
 
-    def prepare_generic_value(self, qname: Optional[str], value: Any) -> Any:
+    def prepare_generic_value(
+        self, qname: Optional[str], value: Any, var: XmlVar
+    ) -> Any:
         """Prepare parsed value before binding to a wildcard field."""
 
         if qname:
@@ -253,7 +256,9 @@ class ElementNode(XmlNode):
 
             if not self.context.class_type.is_model(value):
                 value = any_factory(qname=qname, text=converter.serialize(value))
-            elif not isinstance(value, (any_factory, derived_factory)):
+            elif not isinstance(
+                value, (any_factory, derived_factory)
+            ) and not var.find_choice(qname):
                 meta = self.context.fetch(type(value))
                 xsi_type = namespaces.real_xsi_type(qname, meta.target_qname)
                 value = derived_factory(qname=qname, value=value, type=xsi_type)
diff --git a/xsdata/formats/dataclass/serializers/xml.py b/xsdata/formats/dataclass/serializers/xml.py
index 28fbaa69..0ae7eb0d 100644
--- a/xsdata/formats/dataclass/serializers/xml.py
+++ b/xsdata/formats/dataclass/serializers/xml.py
@@ -118,11 +118,15 @@ 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 for the given var with
-        with xsi abstract type check for non wildcards."""
+        """Produce an events stream from a dataclass for the given var with xsi
+        abstract type check for non wildcards."""
 
         if var.is_wildcard:
-            yield from self.write_dataclass(value, namespace)
+            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)
         else:
             xsi_type = self.xsi_type(var, value, namespace)
             yield from self.write_dataclass(


Test patch:
diff --git a/tests/fixtures/models.py b/tests/fixtures/models.py
index 675fa976..0c44a9c8 100644
--- a/tests/fixtures/models.py
+++ b/tests/fixtures/models.py
@@ -166,7 +166,14 @@ class Paragraph:
 
     content: List[object] = field(
         default_factory=list,
-        metadata=dict(type="Wildcard", namespace="##any", mixed=True),
+        metadata=dict(
+            type="Wildcard",
+            namespace="##any",
+            mixed=True,
+            choices=(
+                dict(name="span", type=Span),
+            ),
+        )
     )
 
 
diff --git a/tests/formats/dataclass/parsers/nodes/test_element.py b/tests/formats/dataclass/parsers/nodes/test_element.py
index f71e0d8e..23c670b8 100644
--- a/tests/formats/dataclass/parsers/nodes/test_element.py
+++ b/tests/formats/dataclass/parsers/nodes/test_element.py
@@ -266,26 +266,38 @@ class ElementNodeTests(FactoryTestCase):
         self.assertEqual(expected, params)
 
     def test_prepare_generic_value(self):
-        actual = self.node.prepare_generic_value(None, 1)
+        var = XmlVarFactory.create(
+            index=2,
+            xml_type=XmlType.WILDCARD,
+            qname="a",
+            types=(object,),
+            elements={"known": XmlVarFactory.create()},
+        )
+
+        actual = self.node.prepare_generic_value(None, 1, var)
         self.assertEqual(1, actual)
 
-        actual = self.node.prepare_generic_value("a", 1)
+        actual = self.node.prepare_generic_value("a", 1, var)
         expected = AnyElement(qname="a", text="1")
         self.assertEqual(expected, actual)
 
-        actual = self.node.prepare_generic_value("a", "foo")
+        actual = self.node.prepare_generic_value("a", "foo", var)
         expected = AnyElement(qname="a", text="foo")
         self.assertEqual(expected, actual)
 
         fixture = make_dataclass("Fixture", [("content", str)])
-        actual = self.node.prepare_generic_value("a", fixture("foo"))
+        actual = self.node.prepare_generic_value("a", fixture("foo"), var)
         expected = DerivedElement(qname="a", value=fixture("foo"), type="Fixture")
         self.assertEqual(expected, actual)
 
-        actual = self.node.prepare_generic_value("a", expected)
+        fixture = make_dataclass("Fixture", [("content", str)])
+        actual = self.node.prepare_generic_value("known", fixture("foo"), var)
+        self.assertEqual(fixture("foo"), actual)
+
+        actual = self.node.prepare_generic_value("a", expected, var)
         self.assertIs(expected, actual)
 
-        actual = self.node.prepare_generic_value("Fixture", fixture("foo"))
+        actual = self.node.prepare_generic_value("Fixture", fixture("foo"), var)
         expected = DerivedElement(qname="Fixture", value=fixture("foo"))
         self.assertEqual(expected, actual)