Repository: tefra/xsdata

Issue:
Parsing XML to a class: A sequence of alternatives is wronly parsed
Using:
- xsdata 24.4
- Python 3.11.5

I ran `xsdata generate my_schema.xsd` on the following schema:

```xsd
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning" vc:minVersion="1.1">
  <xs:element name="RootNode">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="Field" minOccurs="2" maxOccurs="2">
          <xs:alternative test="@name='LeafType1'" type="LeafType1" />
          <xs:alternative test="@name='LeafType2'" type="LeafType2" />
        </xs:element>
      </xs:sequence>
    </xs:complexType>
  </xs:element>

  <xs:complexType name="LeafType1">
    <xs:simpleContent>
      <xs:extension base="xs:unsignedByte">
        <xs:attribute name="name" fixed="LeafType1"/>
      </xs:extension>
    </xs:simpleContent>
  </xs:complexType>
  
  <xs:complexType name="LeafType2">
    <xs:simpleContent>
      <xs:extension base="xs:unsignedByte">
        <xs:attribute name="name" fixed="LeafType2"/>
      </xs:extension>
    </xs:simpleContent>
  </xs:complexType>
</xs:schema>
```

This created my_schema.py (this looks correct):
```python
from dataclasses import dataclass, field
from typing import List, Optional, Union


@dataclass
class LeafType1:
    value: Optional[int] = field(
        default=None,
        metadata={
            "required": True,
        },
    )
    name: str = field(
        init=False,
        default="LeafType1",
        metadata={
            "type": "Attribute",
        },
    )


@dataclass
class LeafType2:
    value: Optional[int] = field(
        default=None,
        metadata={
            "required": True,
        },
    )
    name: str = field(
        init=False,
        default="LeafType2",
        metadata={
            "type": "Attribute",
        },
    )


@dataclass
class RootNode:
    field_value: List[Union[LeafType1, LeafType2]] = field(
        default_factory=list,
        metadata={
            "name": "Field",
            "type": "Element",
            "min_occurs": 2,
            "max_occurs": 2,
        },
    )
```

However when I parsed the following input XML:
```xml
<RootNode>
    <Field name="LeafType1">1</Field>
    <Field name="LeafType2">2</Field>	  
</RootNode>
```

Using the following code:
```python
import pprint
from pathlib import Path
from xsdata.formats.dataclass.parsers import XmlParser
from xsdata.formats.dataclass.parsers.handlers import XmlEventHandler
from my_schema import RootNode


input_xml_path = Path(__file__).parent / "input.xml"
parser = XmlParser(handler=XmlEventHandler)
deserialized = parser.parse(input_xml_path, RootNode)
pprint.pp(deserialized)
```

The output is wrong - we get LeafType1 twice:
```
RootNode(field_value=[LeafType1(value=1, name='LeafType1'),
                      LeafType1(value=2, name='LeafType1')])
```

Here are the files: [xsdata_issue_1012.zip](https://github.com/tefra/xsdata/files/14960327/xsdata_issue_1012.zip)

Discussion:

Patch:
diff --git a/xsdata/formats/converter.py b/xsdata/formats/converter.py
index 31bb4b9c..25864a4e 100644
--- a/xsdata/formats/converter.py
+++ b/xsdata/formats/converter.py
@@ -160,6 +160,11 @@ class ConverterFactory:
             return False
 
         if strict and isinstance(decoded, (float, int, Decimal, XmlPeriod)):
+            if isinstance(decoded, float) and (
+                math.isinf(decoded) or math.isnan(decoded)
+            ):
+                return True
+
             encoded = self.serialize(decoded, **kwargs)
             return value.strip() == encoded
 
diff --git a/xsdata/formats/dataclass/parsers/dict.py b/xsdata/formats/dataclass/parsers/dict.py
index 7f6f6d37..89704cce 100644
--- a/xsdata/formats/dataclass/parsers/dict.py
+++ b/xsdata/formats/dataclass/parsers/dict.py
@@ -132,14 +132,20 @@ class DictDecoder:
         for key, value in data.items():
             var = self.find_var(xml_vars, key, value)
 
-            if var is None and self.config.fail_on_unknown_properties:
-                raise ParserError(f"Unknown property {clazz.__qualname__}.{key}")
+            if var is None:
+                if self.config.fail_on_unknown_properties:
+                    raise ParserError(f"Unknown property {clazz.__qualname__}.{key}")
+                else:
+                    continue
 
-            if var and var.init:
-                if var.wrapper:
-                    value = value[var.local_name]
+            if var.wrapper:
+                value = value[var.local_name]
 
-                params[var.name] = self.bind_value(meta, var, value)
+            value = self.bind_value(meta, var, value)
+            if var.init:
+                params[var.name] = value
+            else:
+                ParserUtils.validate_fixed_value(meta, var, value)
 
         try:
             return self.config.class_factory(clazz, params)
@@ -150,7 +156,7 @@ class DictDecoder:
         """Bind the input data to the given class type.
 
         Examples:
-            >>> {
+            {
                 "qname": "foo",
                 "type": "my:type",
                 "value": {"prop": "value"}
diff --git a/xsdata/formats/dataclass/parsers/nodes/element.py b/xsdata/formats/dataclass/parsers/nodes/element.py
index 24e71f5e..4b9a1f05 100644
--- a/xsdata/formats/dataclass/parsers/nodes/element.py
+++ b/xsdata/formats/dataclass/parsers/nodes/element.py
@@ -1,3 +1,4 @@
+import math
 from typing import Any, Dict, List, Optional, Set, Type
 
 from xsdata.exceptions import ParserError
@@ -191,15 +192,19 @@ class ElementNode(XmlNode):
             var: The xml var instance
             value: The attribute value
         """
+        value = ParserUtils.parse_value(
+            value=value,
+            types=var.types,
+            default=var.default,
+            ns_map=self.ns_map,
+            tokens_factory=var.tokens_factory,
+            format=var.format,
+        )
+
         if var.init:
-            params[var.name] = ParserUtils.parse_value(
-                value=value,
-                types=var.types,
-                default=var.default,
-                ns_map=self.ns_map,
-                tokens_factory=var.tokens_factory,
-                format=var.format,
-            )
+            params[var.name] = value
+        else:
+            ParserUtils.validate_fixed_value(self.meta, var, value)
 
     def bind_any_attr(self, params: Dict, var: XmlVar, qname: str, value: Any):
         """Parse an element attribute to a wildcard field.
@@ -364,18 +369,23 @@ class ElementNode(XmlNode):
         if not var or (text is None and not self.xsi_nil):
             return False
 
+        if self.xsi_nil and not text:
+            value = None
+        else:
+            value = ParserUtils.parse_value(
+                value=text,
+                types=var.types,
+                default=var.default,
+                ns_map=self.ns_map,
+                tokens_factory=var.tokens_factory,
+                format=var.format,
+            )
+
         if var.init:
-            if self.xsi_nil and not text:
-                params[var.name] = None
-            else:
-                params[var.name] = ParserUtils.parse_value(
-                    value=text,
-                    types=var.types,
-                    default=var.default,
-                    ns_map=self.ns_map,
-                    tokens_factory=var.tokens_factory,
-                    format=var.format,
-                )
+            params[var.name] = value
+        else:
+            ParserUtils.validate_fixed_value(self.meta, var, value)
+
         return True
 
     def bind_wild_text(
diff --git a/xsdata/formats/dataclass/parsers/utils.py b/xsdata/formats/dataclass/parsers/utils.py
index 295cac11..305e49a6 100644
--- a/xsdata/formats/dataclass/parsers/utils.py
+++ b/xsdata/formats/dataclass/parsers/utils.py
@@ -1,7 +1,10 @@
+import math
 from collections import UserList
 from typing import Any, Callable, Dict, Iterable, Optional, Sequence, Type
 
+from xsdata.exceptions import ParserError
 from xsdata.formats.converter import QNameConverter, converter
+from xsdata.formats.dataclass.models.elements import XmlMeta, XmlVar
 from xsdata.models.enums import QNames
 from xsdata.utils import collections, constants, text
 from xsdata.utils.namespaces import build_qname
@@ -162,3 +165,31 @@ class ParserUtils:
             value = build_qname(ns_map[prefix], suffix)
 
         return value
+
+    @classmethod
+    def validate_fixed_value(cls, meta: XmlMeta, var: XmlVar, value: Any):
+        """Validate if the parsed value matches the fixed value.
+
+        Special cases
+            - float nans are never equal in python
+            - strings with whitespaces, need trimming
+
+        """
+
+        default_value = var.default() if callable(var.default) else var.default
+        if (
+            isinstance(default_value, float)
+            and isinstance(value, float)
+            and math.isnan(default_value)
+            and math.isnan(value)
+        ) or (
+            isinstance(default_value, str)
+            and isinstance(value, str)
+            and default_value.strip() == value.strip()
+        ):
+            return
+
+        if default_value != value:
+            raise ParserError(
+                f"Fixed value mismatch {meta.qname}:{var.qname}, `{default_value} != {value}`"
+            )
diff --git a/xsdata/models/config.py b/xsdata/models/config.py
index cb3054d9..95d51b9e 100644
--- a/xsdata/models/config.py
+++ b/xsdata/models/config.py
@@ -497,7 +497,7 @@ class GeneratorConfig:
         name = "Config"
         namespace = "http://pypi.org/project/xsdata"
 
-    version: str = attribute(init=False, default=__version__)
+    version: str = attribute(default=__version__)
     output: GeneratorOutput = element(default_factory=GeneratorOutput)
     conventions: GeneratorConventions = element(default_factory=GeneratorConventions)
     substitutions: GeneratorSubstitutions = element(
@@ -542,7 +542,9 @@ class GeneratorConfig:
                 fail_on_converter_warnings=True,
             ),
         )
-        return parser.from_path(path, cls)
+        cfg = parser.from_path(path, cls)
+        cfg.version = __version__
+        return cfg
 
     @classmethod
     def write(cls, output: TextIO, obj: "GeneratorConfig"):


Test patch:
diff --git a/tests/formats/dataclass/parsers/nodes/test_element.py b/tests/formats/dataclass/parsers/nodes/test_element.py
index 750c21d5..a1e5369e 100644
--- a/tests/formats/dataclass/parsers/nodes/test_element.py
+++ b/tests/formats/dataclass/parsers/nodes/test_element.py
@@ -83,7 +83,7 @@ class ElementNodeTests(FactoryTestCase):
         self.node.meta = self.context.build(FixedType)
 
         objects = []
-        self.assertTrue(self.node.bind("foo", "not the fixed value", None, objects))
+        self.assertTrue(self.node.bind("foo", "abc", None, objects))
         self.assertEqual(("foo", FixedType()), objects[-1])
 
     def test_bind_with_derived_element(self):
@@ -182,7 +182,7 @@ class ElementNodeTests(FactoryTestCase):
         self.node.meta = self.context.build(AttrsType)
         self.node.attrs = {
             "index": "0",
-            "fixed": "will be ignored",
+            "fixed": "ignored",
             "{what}ever": "qname",
             "extended": "attr",
         }
@@ -198,7 +198,7 @@ class ElementNodeTests(FactoryTestCase):
         self.node.config.fail_on_unknown_attributes = True
         self.node.attrs = {
             "index": "0",
-            "fixed": "will be ignored",
+            "fixed": "ignored",
             "{what}ever": "qname",
             "extended": "attr",
         }
diff --git a/tests/formats/dataclass/parsers/test_dict.py b/tests/formats/dataclass/parsers/test_dict.py
index dcb363bf..bf9ad8af 100644
--- a/tests/formats/dataclass/parsers/test_dict.py
+++ b/tests/formats/dataclass/parsers/test_dict.py
@@ -12,6 +12,7 @@ from tests.fixtures.models import (
     BaseType,
     ChoiceType,
     ExtendedType,
+    FixedType,
     OptionalChoiceType,
     TypeA,
     TypeB,
@@ -173,6 +174,13 @@ class DictDecoderTests(FactoryTestCase):
         with self.assertRaises(ParserError):
             self.decoder.bind_dataclass({"x": 1, "y": "a"}, TypeD)
 
+    def test_bind_dataclass_with_fixed_field(self):
+        obj = self.decoder.bind_dataclass({"value": "abc"}, FixedType)
+        self.assertEqual("abc", obj.value)
+
+        with self.assertRaises(ParserError):
+            self.decoder.bind_dataclass({"value": "abcd"}, FixedType)
+
     def test_bind_derived_dataclass(self):
         data = {
             "qname": "{urn:books}BookForm",
diff --git a/tests/formats/dataclass/parsers/test_utils.py b/tests/formats/dataclass/parsers/test_utils.py
index 46f81842..78abdf94 100644
--- a/tests/formats/dataclass/parsers/test_utils.py
+++ b/tests/formats/dataclass/parsers/test_utils.py
@@ -1,10 +1,12 @@
 from unittest import mock
 
+from tests.fixtures.models import TypeA
+from xsdata.exceptions import ParserError
 from xsdata.formats.converter import ConverterFactory
 from xsdata.formats.dataclass.context import XmlContext
 from xsdata.formats.dataclass.parsers.utils import ParserUtils
 from xsdata.models.enums import Namespace, QNames
-from xsdata.utils.testing import FactoryTestCase
+from xsdata.utils.testing import FactoryTestCase, XmlMetaFactory, XmlVarFactory
 
 
 class ParserUtilsTests(FactoryTestCase):
@@ -94,3 +96,21 @@ class ParserUtilsTests(FactoryTestCase):
         ns_map["http"] = "happens"
         value = ParserUtils.parse_any_attribute("http://www.com", ns_map)
         self.assertEqual("http://www.com", value)
+
+    def test_validate_fixed_value(self):
+        meta = XmlMetaFactory.create(clazz=TypeA, qname="foo")
+        var = XmlVarFactory.create("fixed", default="a")
+        with self.assertRaises(ParserError) as cm:
+            ParserUtils.validate_fixed_value(meta, var, "b")
+
+        self.assertEqual("Fixed value mismatch foo:fixed, `a != b`", str(cm.exception))
+
+        var = XmlVarFactory.create("fixed", default=lambda: "a")
+        with self.assertRaises(ParserError):
+            ParserUtils.validate_fixed_value(meta, var, "b")
+
+        var = XmlVarFactory.create("fixed", default=lambda: " a ")
+        ParserUtils.validate_fixed_value(meta, var, "  a  ")
+
+        var = XmlVarFactory.create("fixed", default=lambda: float("nan"))
+        ParserUtils.validate_fixed_value(meta, var, float("nan"))
diff --git a/tests/formats/test_converter.py b/tests/formats/test_converter.py
index edd126ed..493b18fc 100644
--- a/tests/formats/test_converter.py
+++ b/tests/formats/test_converter.py
@@ -53,6 +53,18 @@ class ConverterFactoryTests(TestCase):
 
         self.assertTrue(converter.test("0", [float]))
         self.assertFalse(converter.test("0", [float], strict=True))
+
+        self.assertTrue(converter.test("INF", [float], strict=True))
+        self.assertTrue(converter.test("inf", [float], strict=True))
+        self.assertTrue(converter.test("+INF", [float], strict=True))
+        self.assertTrue(converter.test("+inf", [float], strict=True))
+        self.assertTrue(converter.test("-INF", [float], strict=True))
+        self.assertTrue(converter.test("-inf", [float], strict=True))
+        self.assertTrue(converter.test("NAN", [float], strict=True))
+        self.assertTrue(converter.test("NaN", [float], strict=True))
+        self.assertTrue(converter.test("nan", [float], strict=True))
+        self.assertTrue(converter.test("-NAN", [float], strict=True))
+
         self.assertTrue(converter.test(".0", [float]))
         self.assertFalse(converter.test(".0", [float], strict=True))
         self.assertTrue(converter.test("1.0", [float]))