Repository: tefra/xsdata

Issue:
ElementMapper: auto detect simple types
The codegen from xml documents works surprisingly very well so far from all the tests I have done.

One aspect that I left for later was to auto detect simple types, everything defaults to str, it's time to tackle this and also document this feature properly

Discussion:
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

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

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo.png' alt='No Coverage information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=431) No Coverage information  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=431&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=431&metric=new_duplicated_lines_density&view=list)


codecov[bot]: # [Codecov](https://codecov.io/gh/tefra/xsdata/pull/431?src=pr&el=h1) Report
> Merging [#431](https://codecov.io/gh/tefra/xsdata/pull/431?src=pr&el=desc) (6ec2e87) into [master](https://codecov.io/gh/tefra/xsdata/commit/8b325a28f319248e383ff38e774bfbe110fd6722?el=desc) (8b325a2) will **decrease** coverage by `0.04%`.
> The diff coverage is `97.72%`.

[![Impacted file tree graph](https://codecov.io/gh/tefra/xsdata/pull/431/graphs/tree.svg?width=650&height=150&src=pr&token=YzDDLtywvl)](https://codecov.io/gh/tefra/xsdata/pull/431?src=pr&el=tree)

```diff
@@             Coverage Diff             @@
##            master     #431      +/-   ##
===========================================
- Coverage   100.00%   99.95%   -0.05%     
===========================================
  Files           74       74              
  Lines         6867     6875       +8     
  Branches      1212     1215       +3     
===========================================
+ Hits          6867     6872       +5     
- Misses           0        1       +1     
- Partials         0        2       +2     
```


| [Impacted Files](https://codecov.io/gh/tefra/xsdata/pull/431?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [xsdata/codegen/mappers/element.py](https://codecov.io/gh/tefra/xsdata/pull/431/diff?src=pr&el=tree#diff-eHNkYXRhL2NvZGVnZW4vbWFwcGVycy9lbGVtZW50LnB5) | `99.25% <80.00%> (-0.75%)` | :arrow_down: |
| [xsdata/codegen/handlers/attribute\_sanitizer.py](https://codecov.io/gh/tefra/xsdata/pull/431/diff?src=pr&el=tree#diff-eHNkYXRhL2NvZGVnZW4vaGFuZGxlcnMvYXR0cmlidXRlX3Nhbml0aXplci5weQ==) | `100.00% <100.00%> (ø)` | |
| [xsdata/formats/converter.py](https://codecov.io/gh/tefra/xsdata/pull/431/diff?src=pr&el=tree#diff-eHNkYXRhL2Zvcm1hdHMvY29udmVydGVyLnB5) | `100.00% <100.00%> (ø)` | |
| [xsdata/models/enums.py](https://codecov.io/gh/tefra/xsdata/pull/431/diff?src=pr&el=tree#diff-eHNkYXRhL21vZGVscy9lbnVtcy5weQ==) | `100.00% <100.00%> (ø)` | |
| [xsdata/utils/dates.py](https://codecov.io/gh/tefra/xsdata/pull/431/diff?src=pr&el=tree#diff-eHNkYXRhL3V0aWxzL2RhdGVzLnB5) | `100.00% <100.00%> (ø)` | |
| [xsdata/utils/hooks.py](https://codecov.io/gh/tefra/xsdata/pull/431/diff?src=pr&el=tree#diff-eHNkYXRhL3V0aWxzL2hvb2tzLnB5) | `77.77% <0.00%> (-22.23%)` | :arrow_down: |
| [xsdata/codegen/sanitizer.py](https://codecov.io/gh/tefra/xsdata/pull/431/diff?src=pr&el=tree#diff-eHNkYXRhL2NvZGVnZW4vc2FuaXRpemVyLnB5) | `100.00% <0.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tefra/xsdata/pull/431?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/tefra/xsdata/pull/431?src=pr&el=footer). Last update [8b325a2...6ec2e87](https://codecov.io/gh/tefra/xsdata/pull/431?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

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

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo.png' alt='No Coverage information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=431) No Coverage information  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=431&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=431&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

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

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo.png' alt='No Coverage information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=431) No Coverage information  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=431&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=431&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/xsdata/codegen/handlers/attribute_sanitizer.py b/xsdata/codegen/handlers/attribute_sanitizer.py
index e14d9651..f227e8f2 100644
--- a/xsdata/codegen/handlers/attribute_sanitizer.py
+++ b/xsdata/codegen/handlers/attribute_sanitizer.py
@@ -1,16 +1,11 @@
-import warnings
 from dataclasses import dataclass
 from typing import Dict
-from typing import List
-from typing import Optional
-from typing import Type
 
 from xsdata.codegen.mixins import ContainerInterface
 from xsdata.codegen.mixins import HandlerInterface
 from xsdata.codegen.models import Attr
 from xsdata.codegen.models import AttrType
 from xsdata.codegen.models import Class
-from xsdata.exceptions import ConverterWarning
 from xsdata.formats.converter import converter
 from xsdata.models.enums import DataType
 from xsdata.utils import collections
@@ -88,7 +83,7 @@ class AttributeSanitizerHandler(HandlerInterface):
             attr.types = collections.unique_sequence(attr.types, key="qname")
 
     @classmethod
-    def validate_default_value(cls, attr: Attr, ns_map: Dict):
+    def validate_default_value(cls, attr: Attr, ns_map: Dict) -> bool:
         """Reset attribute types when roundtrip conversion of fixed values
         fail."""
         is_enum = attr.is_enumeration
@@ -107,25 +102,7 @@ class AttributeSanitizerHandler(HandlerInterface):
         if is_enum and attr.restrictions.tokens and len(tokens) == 1:
             attr.restrictions.tokens = False
 
-        fmt = attr.restrictions.format
-        return all(cls.match(token, types, ns_map, fmt) for token in tokens)
-
-    @classmethod
-    def match(
-        cls, raw: str, types: List[Type], ns_map: Dict, fmt: Optional[str]
-    ) -> bool:
-        """Check fixed value can be deserialized with the given list of types
-        without warnings and for float/byte instances check that lexical
-        representation matches original string."""
-
-        with warnings.catch_warnings(record=True) as w:
-            decoded = converter.deserialize(raw, types, ns_map=ns_map, format=fmt)
-
-        if w and w[-1].category is ConverterWarning:
-            return False
-
-        if isinstance(decoded, float) and decoded != 0:
-            encoded = converter.serialize(decoded, ns_map=ns_map, format=fmt)
-            return raw.strip() == encoded
-
-        return True
+        return all(
+            converter.test(token, types, ns_map=ns_map, format=attr.restrictions.format)
+            for token in tokens
+        )
diff --git a/xsdata/codegen/mappers/element.py b/xsdata/codegen/mappers/element.py
index 500b717d..a08bb741 100644
--- a/xsdata/codegen/mappers/element.py
+++ b/xsdata/codegen/mappers/element.py
@@ -10,6 +10,7 @@ from typing import Set
 from xsdata.codegen.models import Attr
 from xsdata.codegen.models import AttrType
 from xsdata.codegen.models import Class
+from xsdata.formats.converter import converter
 from xsdata.formats.dataclass.models.generics import AnyElement
 from xsdata.models.enums import DataType
 from xsdata.models.enums import Tag
@@ -116,7 +117,10 @@ class ElementMapper:
             return AttrType(qname=inner.qname, forward=True)
 
         if text:
-            return AttrType(qname=str(DataType.STRING), native=True)
+            for tp in converter.sorted_types():
+                if converter.test(text, [tp], require_namespace=True):
+                    data_type = DataType.from_type(tp)
+                    return AttrType(qname=str(data_type), native=True)
 
         return AttrType(qname=str(DataType.ANY_SIMPLE_TYPE), native=True)
 
diff --git a/xsdata/formats/converter.py b/xsdata/formats/converter.py
index 08547d8e..c078b958 100644
--- a/xsdata/formats/converter.py
+++ b/xsdata/formats/converter.py
@@ -104,6 +104,33 @@ class ConverterFactory:
         instance = self.value_converter(value)
         return instance.serialize(value, **kwargs)
 
+    def test(self, value: Any, types: List[Type], **kwargs: Any) -> bool:
+        """
+        Test the given string value can be parsed using the given list of types
+        without warnings.
+
+        In case of float/int types also very the roundtrip conversion
+        result still matches the original input.
+        """
+
+        if isinstance(value, tuple(types)):
+            return True
+
+        if not isinstance(value, str):
+            return False
+
+        with warnings.catch_warnings(record=True) as w:
+            decoded = self.deserialize(value, types, **kwargs)
+
+        if w and w[-1].category is ConverterWarning:
+            return False
+
+        if isinstance(decoded, (float, int, Decimal, XmlPeriod)):
+            encoded = self.serialize(decoded, **kwargs)
+            return value.strip() == encoded
+
+        return True
+
     def register_converter(self, data_type: Type, func: Union[Callable, Converter]):
         """
         Register a callable or converter for the given data type.
@@ -159,6 +186,10 @@ class ConverterFactory:
 
         return sorted(types, key=lambda x: __PYTHON_TYPES_SORTED__.get(x, 0))
 
+    @classmethod
+    def sorted_types(cls) -> Dict:
+        return __PYTHON_TYPES_SORTED__
+
 
 __PYTHON_TYPES_SORTED__ = {
     int: 1,
@@ -166,11 +197,15 @@ __PYTHON_TYPES_SORTED__ = {
     float: 3,
     Decimal: 4,
     datetime: 5,
-    time: 6,
-    XmlDuration: 7,
-    XmlPeriod: 8,
-    QName: 9,
-    str: 10,
+    date: 6,
+    time: 7,
+    XmlTime: 8,
+    XmlDate: 9,
+    XmlDateTime: 10,
+    XmlDuration: 11,
+    XmlPeriod: 12,
+    QName: 13,
+    str: 14,
 }
 
 
@@ -274,7 +309,11 @@ class DecimalConverter(Converter):
 
 class QNameConverter(Converter):
     def deserialize(
-        self, value: str, ns_map: Optional[Dict] = None, **kwargs: Any
+        self,
+        value: str,
+        ns_map: Optional[Dict] = None,
+        require_namespace: bool = False,
+        **kwargs: Any,
     ) -> QName:
         """
         Convert namespace prefixed strings, or fully qualified strings to
@@ -285,12 +324,12 @@ class QNameConverter(Converter):
             - {foo}bar -> QName("foo", "bar"
         """
         self.validate_input_type(value, str)
-        text_or_uri, tag = self.resolve(value, ns_map)
+        namespace, tag = self.resolve(value, ns_map)
 
-        if text_or_uri:
-            return QName(text_or_uri, tag)
+        if " " in tag or (not namespace and require_namespace):
+            raise ConverterError()
 
-        return QName(tag)
+        return QName(namespace, tag) if namespace else QName(tag)
 
     def serialize(
         self, value: QName, ns_map: Optional[Dict] = None, **kwargs: Any
@@ -319,22 +358,23 @@ class QNameConverter(Converter):
 
     @staticmethod
     def resolve(value: str, ns_map: Optional[Dict]) -> Tuple:
+        value = value.strip()
+
         if not value:
             raise ConverterError("Value is empty")
 
         if value[0] == "{":
-            return value, None
-
-        if ns_map is None:
-            raise ConverterError("QName converter needs ns_map to support prefixes")
+            namespace, name = text.split(value[1:], "}")
 
-        prefix, suffix = text.split(value.strip())
-        namespace = ns_map.get(prefix)
-
-        if prefix and not namespace:
-            raise ConverterError(f"Unknown namespace prefix: `{prefix}`")
+            if not namespace:
+                raise ConverterError("Value is not a QName")
+        else:
+            prefix, name = text.split(value, ":")
+            namespace = ns_map.get(prefix) if ns_map else None
+            if prefix and not namespace:
+                raise ConverterError(f"Unknown namespace prefix: `{prefix}`")
 
-        return namespace, suffix
+        return namespace, name
 
 
 Array = Union[List, Tuple]
diff --git a/xsdata/models/enums.py b/xsdata/models/enums.py
index 25efb57c..2f585a6d 100644
--- a/xsdata/models/enums.py
+++ b/xsdata/models/enums.py
@@ -176,16 +176,15 @@ class DataType(Enum):
     def from_value(cls, value: Any) -> "DataType":
         """Infer the xsd type from the value itself."""
         _type = type(value)
-        datatype = __DataTypeIndex__.get(_type)
-
-        if datatype:
-            return datatype
-
         calculate = __DataTypeInferIndex__.get(_type)
         if calculate:
             return calculate(value)
 
-        return DataType.STRING
+        return cls.from_type(_type)
+
+    @classmethod
+    def from_type(cls, tp: Type) -> "DataType":
+        return __DataTypeIndex__.get(tp, DataType.STRING)
 
     @classmethod
     def from_qname(cls, qname: str) -> Optional["DataType"]:
@@ -218,6 +217,8 @@ def float_datatype(value: float) -> DataType:
 
 __DataTypeIndex__ = {
     bool: DataType.BOOLEAN,
+    int: DataType.INT,
+    float: DataType.FLOAT,
     str: DataType.STRING,
     Decimal: DataType.DECIMAL,
     QName: DataType.QNAME,
@@ -225,6 +226,7 @@ __DataTypeIndex__ = {
     XmlTime: DataType.TIME,
     XmlDateTime: DataType.DATE_TIME,
     XmlDuration: DataType.DURATION,
+    XmlPeriod: DataType.G_YEAR_MONTH,
     # bytes: DataType.HEX_BINARY || DataType.BASE64_BINARY, we can't infer formats
     XmlHexBinary: DataType.HEX_BINARY,
     XmlBase64Binary: DataType.BASE64_BINARY,
diff --git a/xsdata/utils/dates.py b/xsdata/utils/dates.py
index 5e9de509..06a77763 100644
--- a/xsdata/utils/dates.py
+++ b/xsdata/utils/dates.py
@@ -194,7 +194,21 @@ class DateTimeParser:
             self.vidx += 1
             negative = True
 
+        start = self.vidx
         year = self.parse_minimum_digits(4)
+        end = self.vidx
+        raw = self.value[start:end]
+
+        leading_zeros = len(raw) - len(raw.lstrip("0"))
+        if (
+            (leading_zeros == 1 and year > 999)
+            or (leading_zeros == 2 and year > 99)
+            or (leading_zeros == 3 and year > 9)
+            or (leading_zeros == 4 and year > 0)
+            or (leading_zeros > 4)
+        ):
+            raise ValueError()
+
         if negative:
             return -year
 


Test patch:
diff --git a/tests/codegen/handlers/test_attribute_sanitizer.py b/tests/codegen/handlers/test_attribute_sanitizer.py
index abf56e62..48c20ca5 100644
--- a/tests/codegen/handlers/test_attribute_sanitizer.py
+++ b/tests/codegen/handlers/test_attribute_sanitizer.py
@@ -76,8 +76,8 @@ class AttributeSanitizerHandlerTests(FactoryTestCase):
 
         self.processor.process(target)
 
-        self.assertEqual(DataType.FLOAT, attr.types[0].datatype)
-        self.assertIsNotNone(attr.restrictions.format)
+        self.assertEqual(DataType.STRING, attr.types[0].datatype)
+        self.assertIsNone(None, attr.restrictions.format)
         self.assertTrue(attr.restrictions.tokens)
 
         attr.default = "0"
diff --git a/tests/codegen/mappers/test_element.py b/tests/codegen/mappers/test_element.py
index 88901f6f..8e0fcfef 100644
--- a/tests/codegen/mappers/test_element.py
+++ b/tests/codegen/mappers/test_element.py
@@ -62,7 +62,7 @@ class ElementMapperTests(FactoryTestCase):
         third.attrs.append(AttrFactory.create())
         fourth = ClassFactory.create()
 
-        actual = ElementMapper.reduce(iter([first, second, third, fourth]))
+        actual = ElementMapper.reduce([first, second, third, fourth])
 
         self.assertEqual([third, fourth], list(actual))
         mock_merge_attributes.assert_has_calls(
@@ -196,6 +196,13 @@ class ElementMapperTests(FactoryTestCase):
         self.assertEqual(str(DataType.ANY_SIMPLE_TYPE), actual.qname)
         self.assertTrue(actual.native)
 
+        class A:
+            pass
+
+        actual = ElementMapper.build_attribute_type(A(), None)
+        self.assertEqual(str(DataType.ANY_SIMPLE_TYPE), actual.qname)
+        self.assertTrue(actual.native)
+
     def test_add_attribute(self):
         target = ClassFactory.elements(2)
         attr = target.attrs[0].clone()
diff --git a/tests/fixtures/artists/metadata.py b/tests/fixtures/artists/metadata.py
index 6561aac9..7fbf678f 100644
--- a/tests/fixtures/artists/metadata.py
+++ b/tests/fixtures/artists/metadata.py
@@ -1,5 +1,6 @@
 from dataclasses import dataclass, field
-from typing import List, Optional
+from typing import List, Optional, Union
+from xsdata.models.datatype import XmlDate, XmlPeriod
 
 __NAMESPACE__ = "http://musicbrainz.org/ns/mmd-2.0#"
 
@@ -161,19 +162,19 @@ class LifeSpan:
         name = "life-span"
         namespace = "http://musicbrainz.org/ns/mmd-2.0#"
 
-    begin: Optional[str] = field(
+    begin: Optional[Union[XmlDate, XmlPeriod]] = field(
         default=None,
         metadata={
             "type": "Element",
         }
     )
-    end: Optional[str] = field(
+    end: Optional[XmlDate] = field(
         default=None,
         metadata={
             "type": "Element",
         }
     )
-    ended: Optional[str] = field(
+    ended: Optional[bool] = field(
         default=None,
         metadata={
             "type": "Element",
diff --git a/tests/formats/test_converter.py b/tests/formats/test_converter.py
index 8fd2e84a..85626e49 100644
--- a/tests/formats/test_converter.py
+++ b/tests/formats/test_converter.py
@@ -17,7 +17,7 @@ from xsdata.models.datatype import XmlPeriod
 from xsdata.models.enums import UseType
 
 
-class ConverterAdapterTests(TestCase):
+class ConverterFactoryTests(TestCase):
     def test_deserialize(self):
         with warnings.catch_warnings(record=True) as w:
             self.assertEqual("a", converter.deserialize("a", [int]))
@@ -41,6 +41,22 @@ class ConverterAdapterTests(TestCase):
         self.assertEqual("8.77683E-8", converter.serialize(Decimal("8.77683E-8")))
         self.assertEqual("8.77683E-08", converter.serialize(float("8.77683E-8")))
 
+    def test_test(self):
+        self.assertTrue(converter.test(1, [int]))
+        self.assertFalse(converter.test(1, [float]))
+        self.assertFalse(converter.test(None, [int]))
+
+        self.assertFalse(converter.test("a", [int]))
+        self.assertFalse(converter.test("01", [int]))
+        self.assertTrue(converter.test("1", [int]))
+
+        self.assertFalse(converter.test("0", [float]))
+        self.assertFalse(converter.test(".0", [float]))
+        self.assertTrue(converter.test("1.0", [float]))
+
+        self.assertTrue(converter.test("{a}b", [QName]))
+        self.assertFalse(converter.test("{ab", [QName]))
+
     def test_unknown_converter(self):
         class A:
             pass
@@ -290,10 +306,7 @@ class QNameConverterTests(TestCase):
         convert = self.converter.deserialize
         with self.assertRaises(ConverterError) as cm:
             convert("a:b")
-
-        self.assertEqual(
-            "QName converter needs ns_map to support prefixes", str(cm.exception)
-        )
+        self.assertEqual("Unknown namespace prefix: `a`", str(cm.exception))
 
         with self.assertRaises(ConverterError) as cm:
             convert("a:b", ns_map={})
@@ -303,6 +316,10 @@ class QNameConverterTests(TestCase):
             convert("", ns_map={})
         self.assertEqual("Value is empty", str(cm.exception))
 
+        with self.assertRaises(ConverterError) as cm:
+            convert("{a b")
+        self.assertEqual("Value is not a QName", str(cm.exception))
+
         self.assertEqual(QName("a"), convert("a", ns_map={}))
         self.assertEqual(QName("aa", "b"), convert("a:b", ns_map={"a": "aa"}))
         self.assertEqual(QName("{a}b"), convert("{a}b"))
diff --git a/tests/utils/test_dates.py b/tests/utils/test_dates.py
index d785137d..e09965e0 100644
--- a/tests/utils/test_dates.py
+++ b/tests/utils/test_dates.py
@@ -27,13 +27,37 @@ class DatesUtilsTests(TestCase):
         self.assertEqual(134500, next(args))
         self.assertIsNone(next(args, None))
 
-    def test_parse_date_args_negative_year(self):
+    def test_parse_date_args_year(self):
         args = parse_date_args("-1970-05", "%Y-%m")
 
         self.assertEqual(-1970, next(args))
         self.assertEqual(5, next(args))
         self.assertIsNone(next(args, None))
 
+        args = parse_date_args("0009", "%Y")
+        self.assertEqual(9, next(args))
+
+        args = parse_date_args("0099", "%Y")
+        self.assertEqual(99, next(args))
+
+        args = parse_date_args("0999", "%Y")
+        self.assertEqual(999, next(args))
+
+        args = parse_date_args("9999", "%Y")
+        self.assertEqual(9999, next(args))
+
+        with self.assertRaises(ValueError):
+            next(parse_date_args("09999", "%Y"))
+
+        with self.assertRaises(ValueError):
+            next(parse_date_args("00999", "%Y"))
+
+        with self.assertRaises(ValueError):
+            next(parse_date_args("00099", "%Y"))
+
+        with self.assertRaises(ValueError):
+            next(parse_date_args("00009", "%Y"))
+
     def test_parse_date_args_utc_timezone(self):
         args = parse_date_args("Z", "%z")