Repository: tefra/xsdata
Pull request #1042 (2024-05-19 12:23:22)

fix: Avoid using not-threadsafe arnings.catch_warning
## 📒 Description

`warnings.catch_warnings` is not threadsafe. The main converter entry point raises warnings when a value can't be converted to the target types. It's up to the parsers to catch that warning per case and either raise ParserError or ignore them completely.

When we are parsing unions of classes, we intentional trap those warnings in order to find the best candidate, but since catch_warnings is not thread safe, confusing warnings are logged 🤦 

Move the logic to convert warnings to exception from the parsers to the parser utils and avoid using the `catch_warnings` altogether.



Resolves #1041 

## 🔗 What I've Done

> Write a description of the steps taken to resolve the issue

## 💬 Comments

> A place to write any comments to the reviewer.

## 🛫 Checklist

- [ ] Updated docs
- [ ] Added unit-tests
- [ ] [Sample tests](https://github.com/tefra/xsdata-samples) pass
- [ ] [W3C tests](https://github.com/tefra/xsdata-w3c-tests) pass


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=1042) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=1042&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=1042&resolutions=WONTFIX)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=1042&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/no-data-16px.png '') No data about Coverage  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=1042&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=1042)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/tefra/xsdata/pull/1042?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 100.00%. Comparing base [(`c544fbd`)](https://app.codecov.io/gh/tefra/xsdata/commit/c544fbdd1f8ab40153677755ea6423a3ee728f9c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) to head [(`6b0e9a2`)](https://app.codecov.io/gh/tefra/xsdata/pull/1042?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##              main     #1042   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          115       115           
  Lines         9270      9257   -13     
  Branches      2191      2187    -4     
=========================================
- Hits          9270      9257   -13     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/tefra/xsdata/pull/1042?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).

sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=1042) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=1042&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=1042&resolutions=WONTFIX)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=1042&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/no-data-16px.png '') No data about Coverage  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=1042&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=1042)



Patch:
diff --git a/xsdata/formats/dataclass/parsers/bases.py b/xsdata/formats/dataclass/parsers/bases.py
index 97205dc0..9bd4cd68 100644
--- a/xsdata/formats/dataclass/parsers/bases.py
+++ b/xsdata/formats/dataclass/parsers/bases.py
@@ -54,15 +54,11 @@ class NodeParser(PushParser):
         """
         handler = self.handler(clazz=clazz, parser=self)
 
-        with warnings.catch_warnings():
-            if self.config.fail_on_converter_warnings:
-                warnings.filterwarnings("error", category=ConverterWarning)
-
-            try:
-                ns_map = self.ns_map if ns_map is None else ns_map
-                result = handler.parse(source, ns_map)
-            except (ConverterWarning, SyntaxError) as e:
-                raise ParserError(e)
+        try:
+            ns_map = self.ns_map if ns_map is None else ns_map
+            result = handler.parse(source, ns_map)
+        except SyntaxError as e:
+            raise ParserError(e)
 
         if result is not None:
             return result
diff --git a/xsdata/formats/dataclass/parsers/dict.py b/xsdata/formats/dataclass/parsers/dict.py
index d225c90f..5b167fd3 100644
--- a/xsdata/formats/dataclass/parsers/dict.py
+++ b/xsdata/formats/dataclass/parsers/dict.py
@@ -1,10 +1,10 @@
-import warnings
-from dataclasses import dataclass, field
+from contextlib import suppress
+from dataclasses import dataclass, field, replace
 from typing import Any, Dict, Iterable, List, Optional, Type, Union
 
 from typing_extensions import get_args, get_origin
 
-from xsdata.exceptions import ConverterWarning, ParserError
+from xsdata.exceptions import ParserError
 from xsdata.formats.converter import converter
 from xsdata.formats.dataclass.context import XmlContext
 from xsdata.formats.dataclass.models.elements import XmlMeta, XmlVar
@@ -41,18 +41,10 @@ class DictDecoder:
             An instance of the specified class representing the decoded content.
         """
         tp = self.verify_type(clazz, data)
+        if not isinstance(data, list):
+            return self.bind_dataclass(data, tp)
 
-        with warnings.catch_warnings():
-            if self.config.fail_on_converter_warnings:
-                warnings.filterwarnings("error", category=ConverterWarning)
-
-            try:
-                if not isinstance(data, list):
-                    return self.bind_dataclass(data, tp)
-
-                return [self.bind_dataclass(obj, tp) for obj in data]  # type: ignore
-            except ConverterWarning as e:
-                raise ParserError(e)
+        return [self.bind_dataclass(obj, tp) for obj in data]  # type: ignore
 
     def verify_type(self, clazz: Optional[Type[T]], data: Union[Dict, List]) -> Type[T]:
         """Verify the given data matches the given clazz.
@@ -206,12 +198,18 @@ class DictDecoder:
         obj = None
         keys = set(data.keys())
         max_score = -1.0
+        config = replace(self.config, fail_on_converter_warnings=True)
+        decoder = DictDecoder(config=config, context=self.context)
+
         for clazz in classes:
             if not self.context.class_type.is_model(clazz):
                 continue
 
             if self.context.local_names_match(keys, clazz):
-                candidate = self.bind_optional_dataclass(data, clazz)
+                candidate = None
+                with suppress(Exception):
+                    candidate = decoder.bind_dataclass(data, clazz)
+
                 score = self.context.class_type.score_object(candidate)
                 if score > max_score:
                     max_score = score
@@ -225,28 +223,6 @@ class DictDecoder:
             f"to any of the {[cls.__qualname__ for cls in classes]}"
         )
 
-    def bind_optional_dataclass(self, data: Dict, clazz: Type[T]) -> Optional[T]:
-        """Bind the input data to the given class type.
-
-        This is a strict process, if there is any warning the process
-        returns None. This method is used to test if te data fit into
-        the class type.
-
-        Args:
-            data: The derived element dictionary
-            clazz: The target class type to bind the input data
-
-        Returns:
-            An instance of the class type representing the parsed content
-            or None if there is any warning or error.
-        """
-        try:
-            with warnings.catch_warnings():
-                warnings.filterwarnings("error", category=ConverterWarning)
-                return self.bind_dataclass(data, clazz)
-        except Exception:
-            return None
-
     def bind_value(
         self,
         meta: XmlMeta,
@@ -328,6 +304,7 @@ class DictDecoder:
         return ParserUtils.parse_var(
             meta=meta,
             var=var,
+            config=self.config,
             value=value,
             ns_map=EMPTY_MAP,
         )
diff --git a/xsdata/formats/dataclass/parsers/nodes/element.py b/xsdata/formats/dataclass/parsers/nodes/element.py
index c0053a0c..1a280808 100644
--- a/xsdata/formats/dataclass/parsers/nodes/element.py
+++ b/xsdata/formats/dataclass/parsers/nodes/element.py
@@ -196,6 +196,7 @@ class ElementNode(XmlNode):
         value = ParserUtils.parse_var(
             meta=self.meta,
             var=var,
+            config=self.config,
             value=value,
             ns_map=self.ns_map,
         )
@@ -372,6 +373,7 @@ class ElementNode(XmlNode):
             value = ParserUtils.parse_var(
                 meta=self.meta,
                 var=var,
+                config=self.config,
                 value=text,
                 ns_map=self.ns_map,
             )
@@ -518,7 +520,7 @@ class ElementNode(XmlNode):
             )
 
         if not var.any_type and not var.is_wildcard:
-            return nodes.PrimitiveNode(self.meta, var, ns_map)
+            return nodes.PrimitiveNode(self.meta, var, ns_map, self.config)
 
         datatype = DataType.from_qname(xsi_type) if xsi_type else None
         derived = var.is_wildcard
@@ -528,6 +530,7 @@ class ElementNode(XmlNode):
                 var,
                 datatype,
                 ns_map,
+                self.config,
                 var.nillable,
                 derived_factory if derived else None,
             )
diff --git a/xsdata/formats/dataclass/parsers/nodes/primitive.py b/xsdata/formats/dataclass/parsers/nodes/primitive.py
index 8537eb77..f915d416 100644
--- a/xsdata/formats/dataclass/parsers/nodes/primitive.py
+++ b/xsdata/formats/dataclass/parsers/nodes/primitive.py
@@ -2,6 +2,7 @@ from typing import Dict, List, Optional
 
 from xsdata.exceptions import XmlContextError
 from xsdata.formats.dataclass.models.elements import XmlMeta, XmlVar
+from xsdata.formats.dataclass.parsers.config import ParserConfig
 from xsdata.formats.dataclass.parsers.mixins import XmlNode
 from xsdata.formats.dataclass.parsers.utils import ParserUtils
 
@@ -13,14 +14,16 @@ class PrimitiveNode(XmlNode):
         meta: The parent xml meta instance
         var: The xml var instance
         ns_map: The element namespace prefix-URI map
+        config: The parser config instance
     """
 
-    __slots__ = "meta", "var", "ns_map"
+    __slots__ = "meta", "var", "ns_map", "config"
 
-    def __init__(self, meta: XmlMeta, var: XmlVar, ns_map: Dict):
+    def __init__(self, meta: XmlMeta, var: XmlVar, ns_map: Dict, config: ParserConfig):
         self.meta = meta
         self.var = var
         self.ns_map = ns_map
+        self.config = config
 
     def bind(
         self,
@@ -45,7 +48,11 @@ class PrimitiveNode(XmlNode):
             Whether the binding process was successful or not.
         """
         obj = ParserUtils.parse_var(
-            meta=self.meta, var=self.var, value=text, ns_map=self.ns_map
+            meta=self.meta,
+            var=self.var,
+            config=self.config,
+            value=text,
+            ns_map=self.ns_map,
         )
 
         if obj is None and not self.var.nillable:
diff --git a/xsdata/formats/dataclass/parsers/nodes/standard.py b/xsdata/formats/dataclass/parsers/nodes/standard.py
index 297444ab..d919e0e5 100644
--- a/xsdata/formats/dataclass/parsers/nodes/standard.py
+++ b/xsdata/formats/dataclass/parsers/nodes/standard.py
@@ -2,6 +2,7 @@ from typing import Dict, List, Optional, Type
 
 from xsdata.exceptions import XmlContextError
 from xsdata.formats.dataclass.models.elements import XmlMeta, XmlVar
+from xsdata.formats.dataclass.parsers.config import ParserConfig
 from xsdata.formats.dataclass.parsers.mixins import XmlNode
 from xsdata.formats.dataclass.parsers.utils import ParserUtils
 from xsdata.models.enums import DataType
@@ -11,13 +12,24 @@ class StandardNode(XmlNode):
     """XmlNode for elements with a standard xsi:type.
 
     Args:
+        meta: The parent xml meta instance
+        var: The xml var instance
         datatype: The element standard xsi data type
         ns_map: The element namespace prefix-URI map
+        config: The parser config instance
         nillable: Specifies whether nil content is allowed
         derived_factory: The derived element factory
     """
 
-    __slots__ = "meta", "var", "datatype", "ns_map", "nillable", "derived_factory"
+    __slots__ = (
+        "meta",
+        "var",
+        "datatype",
+        "ns_map",
+        "config",
+        "nillable",
+        "derived_factory",
+    )
 
     def __init__(
         self,
@@ -25,6 +37,7 @@ class StandardNode(XmlNode):
         var: XmlVar,
         datatype: DataType,
         ns_map: Dict,
+        config: ParserConfig,
         nillable: bool,
         derived_factory: Optional[Type],
     ):
@@ -32,6 +45,7 @@ class StandardNode(XmlNode):
         self.var = var
         self.datatype = datatype
         self.ns_map = ns_map
+        self.config = config
         self.nillable = nillable
         self.derived_factory = derived_factory
 
@@ -60,6 +74,7 @@ class StandardNode(XmlNode):
         obj = ParserUtils.parse_var(
             meta=self.meta,
             var=self.var,
+            config=self.config,
             value=text,
             types=[self.datatype.type],
             ns_map=self.ns_map,
diff --git a/xsdata/formats/dataclass/parsers/nodes/union.py b/xsdata/formats/dataclass/parsers/nodes/union.py
index fa842df9..68377336 100644
--- a/xsdata/formats/dataclass/parsers/nodes/union.py
+++ b/xsdata/formats/dataclass/parsers/nodes/union.py
@@ -1,15 +1,15 @@
 import copy
-import warnings
-from typing import Any, Dict, List, Optional, Tuple, Type
+from contextlib import suppress
+from dataclasses import replace
+from typing import Any, Dict, List, Optional, Tuple
 
-from xsdata.exceptions import ConverterWarning, ParserError
+from xsdata.exceptions import ParserError
 from xsdata.formats.dataclass.context import XmlContext
 from xsdata.formats.dataclass.models.elements import XmlMeta, 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, XmlNode
 from xsdata.formats.dataclass.parsers.utils import ParserUtils
-from xsdata.formats.types import T
 from xsdata.utils.namespaces import target_uri
 
 
@@ -118,12 +118,26 @@ class UnionNode(XmlNode):
         obj = None
         max_score = -1.0
         parent_namespace = target_uri(qname)
+        config = replace(self.config, fail_on_converter_warnings=True)
+
         for clazz in self.var.types:
-            if self.context.class_type.is_model(clazz):
-                self.context.build(clazz, parent_ns=parent_namespace)
-                candidate = self.parse_class(clazz)
-            else:
-                candidate = self.parse_value(text, [clazz])
+            candidate = None
+            with suppress(Exception):
+                if self.context.class_type.is_model(clazz):
+                    self.context.build(clazz, parent_ns=parent_namespace)
+                    parser = NodeParser(
+                        config=config, context=self.context, handler=EventsHandler
+                    )
+                    candidate = parser.parse(self.events, clazz)
+                else:
+                    candidate = ParserUtils.parse_var(
+                        meta=self.meta,
+                        var=self.var,
+                        config=config,
+                        value=text,
+                        types=[clazz],
+                        ns_map=self.ns_map,
+                    )
 
             score = self.context.class_type.score_object(candidate)
             if score > max_score:
@@ -136,48 +150,3 @@ class UnionNode(XmlNode):
             return True
 
         raise ParserError(f"Failed to parse union node: {self.var.qname}")
-
-    def parse_class(self, clazz: Type[T]) -> Optional[T]:
-        """Replay the recorded events and attempt to build the target class.
-
-        Args:
-            clazz: The target class
-
-        Returns:
-             The target class instance or None if the recorded
-             xml events didn't fit the class.
-        """
-        try:
-            with warnings.catch_warnings():
-                warnings.filterwarnings("error", category=ConverterWarning)
-
-                parser = NodeParser(
-                    config=self.config, context=self.context, handler=EventsHandler
-                )
-                return parser.parse(self.events, clazz)
-        except Exception:
-            return None
-
-    def parse_value(self, value: Any, types: List[Type]) -> Any:
-        """Parse simple values.
-
-        Args:
-            value: The xml value
-            types: The list of the candidate simple types
-
-        Returns:
-            The parsed value or None if value didn't match
-            with any of the given types.
-        """
-        try:
-            with warnings.catch_warnings():
-                warnings.filterwarnings("error", category=ConverterWarning)
-                return ParserUtils.parse_var(
-                    meta=self.meta,
-                    var=self.var,
-                    value=value,
-                    types=types,
-                    ns_map=self.ns_map,
-                )
-        except Exception:
-            return None
diff --git a/xsdata/formats/dataclass/parsers/utils.py b/xsdata/formats/dataclass/parsers/utils.py
index 2d222609..1a774210 100644
--- a/xsdata/formats/dataclass/parsers/utils.py
+++ b/xsdata/formats/dataclass/parsers/utils.py
@@ -1,11 +1,12 @@
 import math
 import warnings
 from collections import UserList
-from typing import Any, Callable, Dict, Iterable, Optional, Sequence, Type, Union
+from typing import Any, Callable, Dict, Iterable, Optional, Sequence, Type
 
 from xsdata.exceptions import ConverterError, ConverterWarning, ParserError
 from xsdata.formats.converter import QNameConverter, converter
 from xsdata.formats.dataclass.models.elements import XmlMeta, XmlVar
+from xsdata.formats.dataclass.parsers.config import ParserConfig
 from xsdata.models.enums import QNames
 from xsdata.utils import collections, constants, text
 from xsdata.utils.namespaces import build_qname
@@ -83,6 +84,7 @@ class ParserUtils:
         cls,
         meta: XmlMeta,
         var: XmlVar,
+        config: ParserConfig,
         value: Any,
         ns_map: Optional[Dict] = None,
         default: Any = None,
@@ -95,6 +97,7 @@ class ParserUtils:
         Args:
             meta: The xml meta instance
             var: The xml var instance
+            config: The parser config instance
             value: A primitive value or a list of primitive values
             ns_map: The element namespace prefix-URI map
             default: Override the var default value
@@ -115,11 +118,11 @@ class ParserUtils:
                 format=format or var.format,
             )
         except ConverterError as ex:
-            message = f"  {ex}"
-            warnings.warn(
-                f"Failed to convert value for `{meta.clazz.__qualname__}.{var.name}`\n{message}",
-                ConverterWarning,
-            )
+            message = f"Failed to convert value for `{meta.clazz.__qualname__}.{var.name}`\n  {ex}"
+            if config.fail_on_converter_warnings:
+                raise ParserError(message)
+
+            warnings.warn(message, ConverterWarning)
 
         return value
 


Test patch:
diff --git a/tests/codegen/handlers/test_disambiguate_choices.py b/tests/codegen/handlers/test_disambiguate_choices.py
index 60fe64c5..29101ab8 100644
--- a/tests/codegen/handlers/test_disambiguate_choices.py
+++ b/tests/codegen/handlers/test_disambiguate_choices.py
@@ -144,7 +144,6 @@ class DisambiguateChoicesTest(FactoryTestCase):
 
     def test_find_ambiguous_choices_ignore_wildcards(self):
         """Wildcards are merged."""
-
         attr = AttrFactory.create()
         attr.choices.append(AttrFactory.any())
         attr.choices.append(AttrFactory.any())
diff --git a/tests/formats/dataclass/parsers/nodes/test_primitive.py b/tests/formats/dataclass/parsers/nodes/test_primitive.py
index ffddb333..61c5b8c2 100644
--- a/tests/formats/dataclass/parsers/nodes/test_primitive.py
+++ b/tests/formats/dataclass/parsers/nodes/test_primitive.py
@@ -3,6 +3,7 @@ from unittest import TestCase, mock
 from tests.fixtures.artists import Artist
 from xsdata.exceptions import XmlContextError
 from xsdata.formats.dataclass.models.elements import XmlType
+from xsdata.formats.dataclass.parsers.config import ParserConfig
 from xsdata.formats.dataclass.parsers.nodes import PrimitiveNode
 from xsdata.formats.dataclass.parsers.utils import ParserUtils
 from xsdata.utils.testing import XmlMetaFactory, XmlVarFactory
@@ -12,6 +13,7 @@ class PrimitiveNodeTests(TestCase):
     def setUp(self):
         super().setUp()
         self.meta = XmlMetaFactory.create(clazz=Artist)
+        self.config = ParserConfig()
 
     @mock.patch.object(ParserUtils, "parse_var")
     def test_bind(self, mock_parse_var):
@@ -20,14 +22,14 @@ class PrimitiveNodeTests(TestCase):
             xml_type=XmlType.TEXT, name="foo", types=(int,), format="Nope"
         )
         ns_map = {"foo": "bar"}
-        node = PrimitiveNode(self.meta, var, ns_map)
+        node = PrimitiveNode(self.meta, var, ns_map, self.config)
         objects = []
 
         self.assertTrue(node.bind("foo", "13", "Impossible", objects))
         self.assertEqual(("foo", 13), objects[-1])
 
         mock_parse_var.assert_called_once_with(
-            meta=self.meta, var=var, value="13", ns_map=ns_map
+            meta=self.meta, var=var, config=self.config, value="13", ns_map=ns_map
         )
 
     def test_bind_nillable_content(self):
@@ -35,7 +37,7 @@ class PrimitiveNodeTests(TestCase):
             xml_type=XmlType.TEXT, name="foo", types=(str,), nillable=False
         )
         ns_map = {"foo": "bar"}
-        node = PrimitiveNode(self.meta, var, ns_map)
+        node = PrimitiveNode(self.meta, var, ns_map, self.config)
         objects = []
 
         self.assertTrue(node.bind("foo", None, None, objects))
@@ -53,7 +55,7 @@ class PrimitiveNodeTests(TestCase):
             nillable=False,
         )
         ns_map = {"foo": "bar"}
-        node = PrimitiveNode(self.meta, var, ns_map)
+        node = PrimitiveNode(self.meta, var, ns_map, self.config)
         objects = []
 
         self.assertTrue(node.bind("foo", None, None, objects))
@@ -66,7 +68,7 @@ class PrimitiveNodeTests(TestCase):
     def test_bind_mixed_with_tail_content(self):
         self.meta.mixed_content = True
         var = XmlVarFactory.create(xml_type=XmlType.TEXT, name="foo", types=(int,))
-        node = PrimitiveNode(self.meta, var, {})
+        node = PrimitiveNode(self.meta, var, {}, self.config)
         objects = []
 
         self.assertTrue(node.bind("foo", "13", "tail", objects))
@@ -76,7 +78,7 @@ class PrimitiveNodeTests(TestCase):
     def test_bind_mixed_without_tail_content(self):
         self.meta.mixed_content = True
         var = XmlVarFactory.create(xml_type=XmlType.TEXT, name="foo", types=(int,))
-        node = PrimitiveNode(self.meta, var, {})
+        node = PrimitiveNode(self.meta, var, {}, self.config)
         objects = []
 
         self.assertTrue(node.bind("foo", "13", "", objects))
@@ -84,7 +86,7 @@ class PrimitiveNodeTests(TestCase):
 
     def test_child(self):
         var = XmlVarFactory.create(xml_type=XmlType.TEXT, name="foo")
-        node = PrimitiveNode(self.meta, var, {})
+        node = PrimitiveNode(self.meta, var, {}, self.config)
 
         with self.assertRaises(XmlContextError):
             node.child("foo", {}, {}, 0)
diff --git a/tests/formats/dataclass/parsers/nodes/test_standard.py b/tests/formats/dataclass/parsers/nodes/test_standard.py
index 4f00d3b9..d63fe1c4 100644
--- a/tests/formats/dataclass/parsers/nodes/test_standard.py
+++ b/tests/formats/dataclass/parsers/nodes/test_standard.py
@@ -3,6 +3,7 @@ from unittest import TestCase
 from tests.fixtures.artists import Artist
 from xsdata.exceptions import XmlContextError
 from xsdata.formats.dataclass.models.generics import DerivedElement
+from xsdata.formats.dataclass.parsers.config import ParserConfig
 from xsdata.formats.dataclass.parsers.nodes import StandardNode
 from xsdata.models.enums import DataType
 from xsdata.utils.testing import XmlMetaFactory, XmlVarFactory
@@ -13,10 +14,13 @@ class StandardNodeTests(TestCase):
         super().setUp()
         self.meta = XmlMetaFactory.create(clazz=Artist)
         self.var = XmlVarFactory.create()
+        self.config = ParserConfig()
 
     def test_bind_simple(self):
         datatype = DataType.INT
-        node = StandardNode(self.meta, self.var, datatype, {}, False, False)
+        node = StandardNode(
+            self.meta, self.var, datatype, {}, self.config, False, False
+        )
         objects = []
 
         self.assertTrue(node.bind("a", "13", None, objects))
@@ -24,7 +28,9 @@ class StandardNodeTests(TestCase):
 
     def test_bind_derived(self):
         datatype = DataType.INT
-        node = StandardNode(self.meta, self.var, datatype, {}, False, DerivedElement)
+        node = StandardNode(
+            self.meta, self.var, datatype, {}, self.config, False, DerivedElement
+        )
         objects = []
 
         self.assertTrue(node.bind("a", "13", None, objects))
@@ -32,7 +38,9 @@ class StandardNodeTests(TestCase):
 
     def test_bind_wrapper_type(self):
         datatype = DataType.HEX_BINARY
-        node = StandardNode(self.meta, self.var, datatype, {}, False, DerivedElement)
+        node = StandardNode(
+            self.meta, self.var, datatype, {}, self.config, False, DerivedElement
+        )
         objects = []
 
         self.assertTrue(node.bind("a", "13", None, objects))
@@ -40,7 +48,7 @@ class StandardNodeTests(TestCase):
 
     def test_bind_nillable(self):
         datatype = DataType.STRING
-        node = StandardNode(self.meta, self.var, datatype, {}, True, None)
+        node = StandardNode(self.meta, self.var, datatype, {}, self.config, True, None)
         objects = []
 
         self.assertTrue(node.bind("a", None, None, objects))
@@ -52,7 +60,9 @@ class StandardNodeTests(TestCase):
 
     def test_child(self):
         datatype = DataType.STRING
-        node = StandardNode(self.meta, self.var, datatype, {}, False, False)
+        node = StandardNode(
+            self.meta, self.var, datatype, {}, self.config, False, False
+        )
 
         with self.assertRaises(XmlContextError):
             node.child("foo", {}, {}, 0)
diff --git a/tests/formats/dataclass/parsers/test_node.py b/tests/formats/dataclass/parsers/test_node.py
index 4497a70c..4bfef17a 100644
--- a/tests/formats/dataclass/parsers/test_node.py
+++ b/tests/formats/dataclass/parsers/test_node.py
@@ -182,7 +182,7 @@ class NodeParserTests(TestCase):
         objects = [("q", "result")]
         queue = []
         var = XmlVarFactory.create(xml_type=XmlType.TEXT, name="foo")
-        queue.append(PrimitiveNode(var, {}, False))
+        queue.append(PrimitiveNode(var, {}, False, parser.config))
 
         self.assertTrue(parser.end(queue, objects, "author", "foobar", None))
         self.assertEqual(0, len(queue))
diff --git a/tests/formats/dataclass/parsers/test_utils.py b/tests/formats/dataclass/parsers/test_utils.py
index 81871bdb..61844489 100644
--- a/tests/formats/dataclass/parsers/test_utils.py
+++ b/tests/formats/dataclass/parsers/test_utils.py
@@ -5,6 +5,7 @@ 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.config import ParserConfig
 from xsdata.formats.dataclass.parsers.utils import ParserUtils
 from xsdata.models.enums import Namespace, QNames
 from xsdata.utils.testing import FactoryTestCase, XmlMetaFactory, XmlVarFactory
@@ -116,17 +117,23 @@ class ParserUtilsTests(FactoryTestCase):
         var = XmlVarFactory.create("fixed", default=lambda: float("nan"))
         ParserUtils.validate_fixed_value(meta, var, float("nan"))
 
-    def test_parse_var_with_warnings(self):
+    def test_parse_var_with_error(self):
         meta = XmlMetaFactory.create(clazz=TypeA, qname="foo")
         var = XmlVarFactory.create("fixed", default="a")
+        config = ParserConfig()
 
         with warnings.catch_warnings(record=True) as w:
-            result = ParserUtils.parse_var(meta, var, "a", types=[int, float])
+            result = ParserUtils.parse_var(meta, var, config, "a", types=[int, float])
 
         expected = (
             "Failed to convert value for `TypeA.fixed`\n"
             "  `a` is not a valid `int | float`"
         )
         self.assertEqual("a", result)
-
         self.assertEqual(expected, str(w[-1].message))
+
+        config.fail_on_converter_warnings = True
+        with self.assertRaises(ParserError) as cm:
+            ParserUtils.parse_var(meta, var, config, "a", types=[int, float])
+
+        self.assertEqual(expected, str(cm.exception))
diff --git a/tests/formats/dataclass/parsers/test_xml.py b/tests/formats/dataclass/parsers/test_xml.py
index 6667569e..5e37f56f 100644
--- a/tests/formats/dataclass/parsers/test_xml.py
+++ b/tests/formats/dataclass/parsers/test_xml.py
@@ -33,7 +33,7 @@ class UserXmlParserTests(FactoryTestCase):
         queue = []
         meta = XmlMetaFactory.create(clazz=Artist)
         var = XmlVarFactory.create(xml_type=XmlType.TEXT, name="foo", types=(bool,))
-        queue.append(PrimitiveNode(meta, var, {}))
+        queue.append(PrimitiveNode(meta, var, {}, self.parser.config))
 
         result = self.parser.end(queue, objects, "enabled", "true", None)
         self.assertTrue(result)