Repository: tefra/xsdata

Issue:
Add instance factory function
I am using XSData to read dataclass instances which need additional checks.

The additional checks should be implemented on user supplied classes which derive from the XSData generated class:

XSData (raw) class:

```
@dataclass
class TModule(TTypeDefinition):
    import_: List[TImport] = field(
        default_factory=list,
        metadata={
            "name": "import",
            "type": "Element",
            "namespace": "http://xxx",
        }
    )
```

My class with additional checks:
```
class TModule(raw.TModule):
    def check(self):
         if condition ...

```

However, the problem is that my TModule is not instanciated during parsing.
Currently, i use a "type hacking approach"
```
@functions_to_super
class TModule(raw.TModule):
    def check(self):
         if condition ...
```
where i copy the defined functions to the base class.
This however is not ideal, since i want to present only my TModule to the user, not the raw classes.

Is there the option (or could this be added) to give a factory function to the parser
```
def createInstance(clazz);
     if clazz.__name__ = "TModule":
        return myTModule()
```
by which i can define for which dataclass which user supplied class is instanciated, so
that the parsed objects dont contain any "raw" classes?

Kind regards

Hauke

 


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=549&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=549&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=549&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=549&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=549&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=549&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=549&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=549&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=549&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=549&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=549&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=549&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=549) 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=549&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=549&metric=new_duplicated_lines_density&view=list)


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

[![Impacted file tree graph](https://codecov.io/gh/tefra/xsdata/pull/549/graphs/tree.svg?width=650&height=150&src=pr&token=YzDDLtywvl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou)](https://codecov.io/gh/tefra/xsdata/pull/549?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou)

```diff
@@             Coverage Diff             @@
##            master     #549      +/-   ##
===========================================
- Coverage   100.00%   99.96%   -0.04%     
===========================================
  Files           93       93              
  Lines         7789     7799      +10     
  Branches      1341     1341              
===========================================
+ Hits          7789     7796       +7     
- Misses           0        2       +2     
- Partials         0        1       +1     
```


| [Impacted Files](https://codecov.io/gh/tefra/xsdata/pull/549?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) | Coverage Δ | |
|---|---|---|
| [...sdata/formats/dataclass/parsers/nodes/primitive.py](https://codecov.io/gh/tefra/xsdata/pull/549/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3BhcnNlcnMvbm9kZXMvcHJpbWl0aXZlLnB5) | `100.00% <ø> (ø)` | |
| [xsdata/formats/dataclass/parsers/nodes/standard.py](https://codecov.io/gh/tefra/xsdata/pull/549/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3BhcnNlcnMvbm9kZXMvc3RhbmRhcmQucHk=) | `100.00% <ø> (ø)` | |
| [xsdata/formats/dataclass/parsers/nodes/wildcard.py](https://codecov.io/gh/tefra/xsdata/pull/549/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3BhcnNlcnMvbm9kZXMvd2lsZGNhcmQucHk=) | `100.00% <ø> (ø)` | |
| [xsdata/formats/dataclass/parsers/config.py](https://codecov.io/gh/tefra/xsdata/pull/549/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3BhcnNlcnMvY29uZmlnLnB5) | `100.00% <100.00%> (ø)` | |
| [xsdata/formats/dataclass/parsers/json.py](https://codecov.io/gh/tefra/xsdata/pull/549/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3BhcnNlcnMvanNvbi5weQ==) | `100.00% <100.00%> (ø)` | |
| [xsdata/formats/dataclass/parsers/nodes/element.py](https://codecov.io/gh/tefra/xsdata/pull/549/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/parsers/nodes/union.py](https://codecov.io/gh/tefra/xsdata/pull/549/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3BhcnNlcnMvbm9kZXMvdW5pb24ucHk=) | `100.00% <100.00%> (ø)` | |
| [xsdata/formats/dataclass/typing.py](https://codecov.io/gh/tefra/xsdata/pull/549/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3R5cGluZy5weQ==) | `97.19% <0.00%> (-2.81%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tefra/xsdata/pull/549?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/tefra/xsdata/pull/549?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou). Last update [4eadb4a...231b8c3](https://codecov.io/gh/tefra/xsdata/pull/549?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).

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=549&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=549&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=549&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=549&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=549&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=549&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=549&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=549&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=549&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=549&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=549&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=549&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=549) 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=549&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=549&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c6522c77..61a7e8db 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,7 +2,7 @@ exclude: tests/fixtures
 
 repos:
   - repo: https://github.com/asottile/pyupgrade
-    rev: v2.19.4
+    rev: v2.20.0
     hooks:
       - id: pyupgrade
         args: [--py37-plus]
diff --git a/docs/examples.rst b/docs/examples.rst
index 7709e186..a078ca04 100644
--- a/docs/examples.rst
+++ b/docs/examples.rst
@@ -22,6 +22,7 @@ Advance Topics
     :maxdepth: 1
 
     examples/custom-property-names
+    examples/custom-class-factory
 
 
 Test Suites
diff --git a/docs/examples/custom-class-factory.rst b/docs/examples/custom-class-factory.rst
new file mode 100644
index 00000000..63bd1a8a
--- /dev/null
+++ b/docs/examples/custom-class-factory.rst
@@ -0,0 +1,35 @@
+====================
+Custom class factory
+====================
+
+
+It's not recommended to modify the generated models. If you need to add any pre/post
+initialization logic or even validations you can use the parser config to override the
+default class factory.
+
+.. doctest::
+
+    >>> from dataclasses import dataclass
+    >>> from xsdata.formats.dataclass.parsers import JsonParser
+    >>> from xsdata.formats.dataclass.parsers.config import ParserConfig
+    ...
+    >>> def custom_class_factory(clazz, params):
+    ...     if clazz.__name__ == "Person":
+    ...         return clazz(**{k: v.upper() for k, v in params.items()})
+    ...
+    ...     return clazz(**params)
+    ...
+
+    >>> config = ParserConfig(class_factory=custom_class_factory)
+    >>> parser = JsonParser(config=config)
+    ...
+    >>> @dataclass
+    ... class Person:
+    ...     first_name: str
+    ...     last_name: str
+    ...
+    >>> json_str = """{"first_name": "chris", "last_name": "foo"}"""
+    ...
+    ...
+    >>> print(parser.from_string(json_str, Person))
+    Person(first_name='CHRIS', last_name='FOO')
diff --git a/xsdata/formats/dataclass/parsers/config.py b/xsdata/formats/dataclass/parsers/config.py
index fd2ae23b..2c0d776c 100644
--- a/xsdata/formats/dataclass/parsers/config.py
+++ b/xsdata/formats/dataclass/parsers/config.py
@@ -1,5 +1,15 @@
 from dataclasses import dataclass
+from typing import Callable
+from typing import Dict
 from typing import Optional
+from typing import Type
+from typing import TypeVar
+
+T = TypeVar("T")
+
+
+def default_class_factory(cls: Type[T], params: Dict) -> T:
+    return cls(**params)  # type: ignore
 
 
 @dataclass
@@ -10,6 +20,7 @@ class ParserConfig:
     :param base_url: Specify a base URL when parsing from memory and
         you need support for relative links eg xinclude
     :param process_xinclude: Enable xinclude statements processing
+    :param class_factory: Override default object instantiation
     :param fail_on_unknown_properties: Skip unknown properties or
         fail with exception
     :param fail_on_converter_warnings: Turn converter warnings to
@@ -18,5 +29,6 @@ class ParserConfig:
 
     base_url: Optional[str] = None
     process_xinclude: bool = False
+    class_factory: Callable[[Type[T], Dict], T] = default_class_factory
     fail_on_unknown_properties: bool = True
     fail_on_converter_warnings: bool = False
diff --git a/xsdata/formats/dataclass/parsers/json.py b/xsdata/formats/dataclass/parsers/json.py
index 0612b96a..711ffb4f 100644
--- a/xsdata/formats/dataclass/parsers/json.py
+++ b/xsdata/formats/dataclass/parsers/json.py
@@ -90,8 +90,7 @@ class JsonParser(AbstractParser):
         if list_type != isinstance(data, list):
             if list_type:
                 raise ParserError("Document is object, expected array")
-            else:
-                raise ParserError("Document is array, expected object")
+            raise ParserError("Document is array, expected object")
 
         return clazz  # type: ignore
 
@@ -128,7 +127,7 @@ class JsonParser(AbstractParser):
                 params[var.name] = self.bind_value(meta, var, value)
 
         try:
-            return clazz(**params)  # type: ignore
+            return self.config.class_factory(clazz, params)
         except TypeError as e:
             raise ParserError(e)
 
@@ -257,22 +256,22 @@ class JsonParser(AbstractParser):
         if var.is_clazz_union:
             # Union of dataclasses
             return self.bind_best_dataclass(data, var.types)
-        elif var.elements:
+        if var.elements:
             # Compound field with multiple choices
             return self.bind_best_dataclass(data, var.element_types)
-        elif var.any_type or var.is_wildcard:
+        if var.any_type or var.is_wildcard:
             # xs:anyType element, check all meta classes
             return self.bind_best_dataclass(data, meta.element_types)
-        else:
-            assert var.clazz is not None
 
-            subclasses = set(self.context.get_subclasses(var.clazz))
-            if subclasses:
-                # field annotation is an abstract/base type
-                subclasses.add(var.clazz)
-                return self.bind_best_dataclass(data, subclasses)
+        assert var.clazz is not None
+
+        subclasses = set(self.context.get_subclasses(var.clazz))
+        if subclasses:
+            # field annotation is an abstract/base type
+            subclasses.add(var.clazz)
+            return self.bind_best_dataclass(data, subclasses)
 
-            return self.bind_dataclass(data, var.clazz)
+        return self.bind_dataclass(data, var.clazz)
 
     def bind_derived_value(self, meta: XmlMeta, var: XmlVar, data: Dict) -> T:
         """Bind derived element entry point."""
diff --git a/xsdata/formats/dataclass/parsers/nodes/element.py b/xsdata/formats/dataclass/parsers/nodes/element.py
index bb09b264..21318452 100644
--- a/xsdata/formats/dataclass/parsers/nodes/element.py
+++ b/xsdata/formats/dataclass/parsers/nodes/element.py
@@ -31,7 +31,7 @@ class ElementNode(XmlNode):
     :param context: Model context provider
     :param position: The node position of objects cache
     :param mixed: The node supports mixed content
-    :param derived: The xml element is derived from a base type
+    :param derived_factory: Derived element factory
     :param xsi_type: The xml type substitution
     """
 
@@ -92,7 +92,7 @@ class ElementNode(XmlNode):
             if isinstance(params[key], PendingCollection):
                 params[key] = params[key].evaluate()
 
-        obj = self.meta.clazz(**params)
+        obj = self.config.class_factory(self.meta.clazz, params)
         if self.derived_factory:
             obj = self.derived_factory(qname=qname, value=obj, type=self.xsi_type)
 
@@ -330,6 +330,7 @@ class ElementNode(XmlNode):
                 var=var,
                 attrs=attrs,
                 ns_map=ns_map,
+                config=self.config,
                 context=self.context,
                 position=position,
             )
diff --git a/xsdata/formats/dataclass/parsers/nodes/primitive.py b/xsdata/formats/dataclass/parsers/nodes/primitive.py
index 028be68b..1b9fb01a 100644
--- a/xsdata/formats/dataclass/parsers/nodes/primitive.py
+++ b/xsdata/formats/dataclass/parsers/nodes/primitive.py
@@ -15,6 +15,7 @@ class PrimitiveNode(XmlNode):
 
     :param var: Class field xml var instance
     :param ns_map: Namespace prefix-URI map
+    :param derived_factory: Derived element factory
     """
 
     __slots__ = "var", "ns_map", "derived_factory"
diff --git a/xsdata/formats/dataclass/parsers/nodes/standard.py b/xsdata/formats/dataclass/parsers/nodes/standard.py
index 3542e41c..e04f364a 100644
--- a/xsdata/formats/dataclass/parsers/nodes/standard.py
+++ b/xsdata/formats/dataclass/parsers/nodes/standard.py
@@ -15,9 +15,8 @@ class StandardNode(XmlNode):
 
     :param datatype: Standard xsi data type
     :param ns_map: Namespace prefix-URI map
-    :param derived: Specify whether the value needs to be wrapped with
-        :class:`~xsdata.formats.dataclass.models.generics.DerivedElement`
     :param nillable: Specify whether the node supports nillable content
+    :param derived_factory: Optional derived element factory
     """
 
     __slots__ = "datatype", "ns_map", "nillable", "derived_factory"
diff --git a/xsdata/formats/dataclass/parsers/nodes/union.py b/xsdata/formats/dataclass/parsers/nodes/union.py
index dd5e66c1..f87ea7ed 100644
--- a/xsdata/formats/dataclass/parsers/nodes/union.py
+++ b/xsdata/formats/dataclass/parsers/nodes/union.py
@@ -13,6 +13,7 @@ from xsdata.formats.bindings import T
 from xsdata.formats.dataclass.context import XmlContext
 from xsdata.formats.dataclass.models.elements import XmlVar
 from xsdata.formats.dataclass.parsers.bases import NodeParser
+from xsdata.formats.dataclass.parsers.config import ParserConfig
 from xsdata.formats.dataclass.parsers.mixins import EventsHandler
 from xsdata.formats.dataclass.parsers.mixins import XmlNode
 from xsdata.formats.dataclass.parsers.utils import ParserUtils
@@ -32,18 +33,35 @@ class UnionNode(XmlNode):
     :param attrs: Key-value attribute mapping
     :param ns_map: Namespace prefix-URI map
     :param position: The node position of objects cache
+    :param config: Parser configuration
     :param context: Model context provider
     """
 
-    __slots__ = "var", "attrs", "ns_map", "position", "context", "level", "events"
+    __slots__ = (
+        "var",
+        "attrs",
+        "ns_map",
+        "position",
+        "config",
+        "context",
+        "level",
+        "events",
+    )
 
     def __init__(
-        self, var: XmlVar, attrs: Dict, ns_map: Dict, position: int, context: XmlContext
+        self,
+        var: XmlVar,
+        attrs: Dict,
+        ns_map: Dict,
+        position: int,
+        config: ParserConfig,
+        context: XmlContext,
     ):
         self.var = var
         self.attrs = attrs
         self.ns_map = ns_map
         self.position = position
+        self.config = config
         self.context = context
         self.level = 0
         self.events: List[Tuple[str, str, Any, Any]] = []
@@ -94,7 +112,9 @@ class UnionNode(XmlNode):
             with warnings.catch_warnings():
                 warnings.filterwarnings("error", category=ConverterWarning)
 
-                parser = NodeParser(context=self.context, handler=EventsHandler)
+                parser = NodeParser(
+                    config=self.config, context=self.context, handler=EventsHandler
+                )
                 return parser.parse(self.events, clazz)
         except Exception:
             return None
diff --git a/xsdata/formats/dataclass/parsers/nodes/wildcard.py b/xsdata/formats/dataclass/parsers/nodes/wildcard.py
index 8b03d5cb..5d00100e 100644
--- a/xsdata/formats/dataclass/parsers/nodes/wildcard.py
+++ b/xsdata/formats/dataclass/parsers/nodes/wildcard.py
@@ -20,6 +20,7 @@ class WildcardNode(XmlNode):
     :param attrs: Key-value attribute mapping
     :param ns_map: Namespace prefix-URI map
     :param position: The node position of objects cache
+    :param factory: Wildcard element factory
     """
 
     __slots__ = "var", "attrs", "ns_map", "position", "factory"


Test patch:
diff --git a/tests/formats/dataclass/parsers/nodes/test_union.py b/tests/formats/dataclass/parsers/nodes/test_union.py
index f2eae99c..5c233d7f 100644
--- a/tests/formats/dataclass/parsers/nodes/test_union.py
+++ b/tests/formats/dataclass/parsers/nodes/test_union.py
@@ -6,18 +6,31 @@ from tests.fixtures.models import UnionType
 from xsdata.exceptions import ParserError
 from xsdata.formats.dataclass.context import XmlContext
 from xsdata.formats.dataclass.models.elements import XmlType
+from xsdata.formats.dataclass.parsers.config import ParserConfig
 from xsdata.formats.dataclass.parsers.nodes import UnionNode
 from xsdata.models.mixins import attribute
 from xsdata.utils.testing import XmlVarFactory
 
 
 class UnionNodeTests(TestCase):
+    def setUp(self) -> None:
+        super().setUp()
+
+        self.context = XmlContext()
+        self.config = ParserConfig()
+
     def test_child(self):
         attrs = {"id": "1"}
         ns_map = {"ns0": "xsdata"}
-        ctx = XmlContext()
         var = XmlVarFactory.create(xml_type=XmlType.TEXT, name="foo", qname="foo")
-        node = UnionNode(position=0, var=var, context=ctx, attrs={}, ns_map={})
+        node = UnionNode(
+            position=0,
+            var=var,
+            config=self.config,
+            context=self.context,
+            attrs={},
+            ns_map={},
+        )
         self.assertEqual(node, node.child("foo", attrs, ns_map, 10))
 
         self.assertEqual(1, node.level)
@@ -25,9 +38,15 @@ class UnionNodeTests(TestCase):
         self.assertIsNot(attrs, node.events[0][2])
 
     def test_bind_appends_end_event_when_level_not_zero(self):
-        ctx = XmlContext()
         var = XmlVarFactory.create(xml_type=XmlType.TEXT, name="foo", qname="foo")
-        node = UnionNode(position=0, var=var, context=ctx, attrs={}, ns_map={})
+        node = UnionNode(
+            position=0,
+            var=var,
+            config=self.config,
+            context=self.context,
+            attrs={},
+            ns_map={},
+        )
         node.level = 1
         objects = []
 
@@ -43,12 +62,18 @@ class UnionNodeTests(TestCase):
         item2 = make_dataclass("Item2", [("a", int, attribute())])
         root = make_dataclass("Root", [("item", Union[str, int, item2, item])])
 
-        ctx = XmlContext()
-        meta = ctx.build(root)
+        meta = self.context.build(root)
         var = next(meta.find_children("item"))
         attrs = {"a": "1", "b": 2}
         ns_map = {}
-        node = UnionNode(position=0, var=var, context=ctx, attrs=attrs, ns_map=ns_map)
+        node = UnionNode(
+            position=0,
+            var=var,
+            config=self.config,
+            context=self.context,
+            attrs=attrs,
+            ns_map=ns_map,
+        )
         objects = []
 
         self.assertTrue(node.bind("item", "1", None, objects))
@@ -73,11 +98,17 @@ class UnionNodeTests(TestCase):
         self.assertEqual("a", objects[-1][1])
 
     def test_bind_raises_parser_error_on_failure(self):
-        ctx = XmlContext()
-        meta = ctx.build(UnionType)
+        meta = self.context.build(UnionType)
         var = next(meta.find_children("element"))
 
-        node = UnionNode(position=0, var=var, context=ctx, attrs={}, ns_map={})
+        node = UnionNode(
+            position=0,
+            var=var,
+            config=self.config,
+            context=self.context,
+            attrs={},
+            ns_map={},
+        )
 
         with self.assertRaises(ParserError) as cm:
             node.bind("element", None, None, [])