Repository: tefra/xsdata

Issue:
Allow serialization of dataclass objects from and to dict
### Allow serialization of dataclass objects from and to dict

Currently I know only two workarounds.
After explaining them I will ask my questions at the end ;-)

The first workarounds involves using `json.loads` and `json.dumps`:
````
def model_to_dict(model):
    json_serializer = JsonSerializer()
    json_string = json_serializer.render(model)
    return json.loads(json_string)

def dict_to_model(a_dict):
    json_string = json.dumps(a_dict)
    json_parser = JsonParser()
    return json_parser.parse(json_string, generated.Model)
````

This works but requires the extra step of creating a json string.

The second workaround exploits the fact that xsdata uses dicts internally.

For serialization this is kind of supported and documented:
````
def model_to_dict(model):
    result = {}

    def dict_dumper(obj, *_args, **_kwargs):
        result.update(obj)

    json_serializer = JsonSerializer(dump_factory=dict_dumper)
    json_serializer.write(None, model)

    return result
````

For parsing it exploits undocumented internals:
````
def dict_to_model(a_dict):
    class FakeSourceHavingReadAttribute:
        def read(self):
            pass

    def dict_loader(_src):
        return a_dict

    json_parser = JsonParser(load_factory=dict_loader)
    model = json_parser.parse(FakeSourceHavingReadAttribute(), generated.Model)

    return model
````

Two questions
1. Is there a better way?
2. Should we modify `JsonParser` to get rid of the `FakeSourceHavingReadAttribute` hack?


Discussion:
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=939) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=939&resolved=false&inNewCodePeriod=true)

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=939&resolved=false&inNewCodePeriod=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=939&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=939)


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=939) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=939&resolved=false&inNewCodePeriod=true)

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=939&resolved=false&inNewCodePeriod=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=939&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=939)



Patch:
diff --git a/docs/data_binding/dict_decoding.md b/docs/data_binding/dict_decoding.md
new file mode 100644
index 00000000..6f6cbfc4
--- /dev/null
+++ b/docs/data_binding/dict_decoding.md
@@ -0,0 +1,65 @@
+# Dict Decoding
+
+The [DictDecoder][xsdata.formats.dataclass.parsers.DictDecoder] can be used to bind a
+dictionary or a list of dictionaries to data models. It's the backbone of the
+[JsonParser][xsdata.formats.dataclass.parsers.JsonParser] without the loading
+entrypoints.
+
+## Single object
+
+```python
+>>> from xsdata.formats.dataclass.context import XmlContext
+>>> from xsdata.formats.dataclass.parsers import DictDecoder
+>>> from xsdata.formats.dataclass.parsers.config import ParserConfig
+>>> from tests.fixtures.books.books import BookForm
+>>>
+>>> config = ParserConfig()
+>>> context = XmlContext()
+>>> decoder = DictDecoder(context=context, config=config)
+>>>
+>>> data = {
+...     "author": "Hightower, Kim",
+...     "title": "The First Book",
+...     "genre": "Fiction",
+...     "price": 44.95,
+...     "pub_date": "2000-10-01",
+...     "review": "An amazing story of nothing.",
+...     "id": "bk001"
+... }
+>>> book = decoder.decode(data, BookForm)
+>>> book.author
+'Hightower, Kim'
+
+```
+
+## List of objects
+
+```python
+>>> from typing import List
+>>> from tests.fixtures.books.books import BookForm
+>>>
+>>> data = [
+...     {
+...       "author": "Hightower, Kim",
+...       "title": "The First Book",
+...       "genre": "Fiction",
+...       "price": 44.95,
+...       "pub_date": "2000-10-01",
+...       "review": "An amazing story of nothing.",
+...       "id": "bk001"
+...     },
+...     {
+...       "author": "Nagata, Suanne",
+...       "title": "Becoming Somebody",
+...       "genre": "Biography",
+...       "price": None,
+...       "pub_date": None,
+...       "review": "A masterpiece of the fine art of gossiping.",
+...       "id": "bk002"
+...     }
+...   ]
+>>> booklist = decoder.decode(data, List[BookForm])
+>>> booklist[1].author
+'Nagata, Suanne'
+
+```
diff --git a/docs/data_binding/dict_encoding.md b/docs/data_binding/dict_encoding.md
new file mode 100644
index 00000000..7585c7fb
--- /dev/null
+++ b/docs/data_binding/dict_encoding.md
@@ -0,0 +1,75 @@
+# Dict Encoding
+
+The [DictEncoder][xsdata.formats.dataclass.serializers.DictEncoder] converts a data
+model instance to a dictionary and encodes all values to make them safe for
+serialization.
+
+```python
+>>> from xsdata.formats.dataclass.context import XmlContext
+>>> from xsdata.formats.dataclass.serializers import DictEncoder
+>>> from xsdata.formats.dataclass.serializers.config import SerializerConfig
+
+>>> config = SerializerConfig(pretty_print=True)
+>>> context = XmlContext()
+>>> encoder = DictEncoder()
+>>> encoder = DictEncoder(context=context, config=config)
+
+```
+
+## Example
+
+```python
+>>> import pprint
+>>> from tests.fixtures.books import BookForm
+>>>
+>>> book = BookForm(
+...     id="bk001",
+...     author="Hightower, Kim",
+...     title="The First Book",
+...     genre="Fiction",
+...     price=44.95,
+...     review="An amazing story of nothing.",
+... )
+>>> pprint.pprint(encoder.encode(book))
+{'author': 'Hightower, Kim',
+ 'genre': 'Fiction',
+ 'id': 'bk001',
+ 'lang': 'en',
+ 'price': 44.95,
+ 'pub_date': None,
+ 'review': 'An amazing story of nothing.',
+ 'title': 'The First Book'}
+
+```
+
+## Custom dict factory
+
+By using a custom dict factory you can change the output behaviour, like filter out
+`None` values.
+
+```python
+>>> from typing import Dict, Tuple
+>>>
+>>> def filter_none(x: Tuple) -> Dict:
+...     return {k: v for k, v in x if v is not None}
+>>>
+>>> encoder = DictEncoder(dict_factory=filter_none)
+>>> pprint.pprint(encoder.encode(book))
+{'author': 'Hightower, Kim',
+ 'genre': 'Fiction',
+ 'id': 'bk001',
+ 'lang': 'en',
+ 'price': 44.95,
+ 'review': 'An amazing story of nothing.',
+ 'title': 'The First Book'}
+
+```
+
+or conveniently
+
+```python
+>>> from xsdata.formats.dataclass.serializers import DictFactory
+>>>
+>>> encoder = DictEncoder(dict_factory=DictFactory.FILTER_NONE)
+
+```
diff --git a/docs/data_binding/json_serializing.md b/docs/data_binding/json_serializing.md
index b47d9ab8..e9761813 100644
--- a/docs/data_binding/json_serializing.md
+++ b/docs/data_binding/json_serializing.md
@@ -133,7 +133,7 @@ By using a custom dict factory you can change the output behaviour, like filter
 or conveniently
 
 ```python
->>> from xsdata.formats.dataclass.serializers.json import DictFactory
+>>> from xsdata.formats.dataclass.serializers import DictFactory
 >>>
 >>> serializer = JsonSerializer(dict_factory=DictFactory.FILTER_NONE)
 
diff --git a/mkdocs.yml b/mkdocs.yml
index 3610da31..d4ef441b 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -40,7 +40,6 @@ theme:
     - navigation.top
     - content.tabs.link
     - search.suggest
-    - toc.integrate
   logo: 'logo-small.svg'
   favicon: 'favicon.png'
 
@@ -117,6 +116,8 @@ nav:
   - JSON Parsing: data_binding/json_parsing.md
   - JSON Serializing: data_binding/json_serializing.md
   - Pycode Serializing: data_binding/pycode_serializing.md
+  - Dict Decoding: data_binding/dict_decoding.md
+  - Dict Encoding: data_binding/dict_encoding.md
 - FAQ: faq.md
 - API: api/
 - Plugins:
diff --git a/xsdata/formats/dataclass/client.py b/xsdata/formats/dataclass/client.py
index 2e53bf20..964d5483 100644
--- a/xsdata/formats/dataclass/client.py
+++ b/xsdata/formats/dataclass/client.py
@@ -1,9 +1,8 @@
-from dataclasses import dataclass, field
 from typing import Any, Dict, NamedTuple, Optional, Type, Union
 
 from xsdata.exceptions import ClientValueError
-from xsdata.formats.dataclass.parsers import XmlParser
-from xsdata.formats.dataclass.parsers.json import DictConverter
+from xsdata.formats.dataclass.context import XmlContext
+from xsdata.formats.dataclass.parsers import DictDecoder, XmlParser
 from xsdata.formats.dataclass.serializers import XmlSerializer
 from xsdata.formats.dataclass.transports import DefaultTransport, Transport
 
@@ -59,7 +58,6 @@ class TransportTypes:
     SOAP = "http://schemas.xmlsoap.org/soap/http"
 
 
-@dataclass
 class Client:
     """A wsdl client.
 
@@ -70,11 +68,31 @@ class Client:
         serializer: The xml serializer instance
     """
 
-    config: Config
-    transport: Transport = field(default_factory=DefaultTransport)
-    parser: XmlParser = field(default_factory=XmlParser)
-    serializer: XmlSerializer = field(default_factory=XmlSerializer)
-    dict_converter: DictConverter = field(init=False, default_factory=DictConverter)
+    __slots__ = "config", "transport", "parser", "serializer"
+
+    def __init__(
+        self,
+        config: Config,
+        transport: Optional[Transport] = None,
+        parser: Optional[XmlParser] = None,
+        serializer: Optional[XmlSerializer] = None,
+    ):
+        self.config = config
+        self.transport = transport or DefaultTransport()
+
+        if not serializer and not parser:
+            context = XmlContext()
+            serializer = XmlSerializer(context=context)
+            parser = XmlParser(context=context)
+        elif not serializer:
+            assert parser is not None
+            serializer = XmlSerializer(context=parser.context)
+        elif not parser:
+            assert serializer is not None
+            parser = XmlParser(context=serializer.context)
+
+        self.parser = parser
+        self.serializer = serializer
 
     @classmethod
     def from_service(cls, obj: Type, **kwargs: Any) -> "Client":
@@ -159,7 +177,8 @@ class Client:
             ClientValueError: If the config input type doesn't match the given object.
         """
         if isinstance(obj, Dict):
-            obj = self.dict_converter.convert(obj, self.config.input)
+            decoder = DictDecoder(context=self.serializer.context)
+            obj = decoder.decode(obj, self.config.input)
 
         if not isinstance(obj, self.config.input):
             raise ClientValueError(
diff --git a/xsdata/formats/dataclass/parsers/__init__.py b/xsdata/formats/dataclass/parsers/__init__.py
index 36fa8b64..d46d697f 100644
--- a/xsdata/formats/dataclass/parsers/__init__.py
+++ b/xsdata/formats/dataclass/parsers/__init__.py
@@ -1,5 +1,12 @@
+from xsdata.formats.dataclass.parsers.dict import DictDecoder
 from xsdata.formats.dataclass.parsers.json import JsonParser
 from xsdata.formats.dataclass.parsers.tree import TreeParser
 from xsdata.formats.dataclass.parsers.xml import UserXmlParser, XmlParser
 
-__all__ = ["JsonParser", "XmlParser", "UserXmlParser", "TreeParser"]
+__all__ = [
+    "DictDecoder",
+    "JsonParser",
+    "XmlParser",
+    "UserXmlParser",
+    "TreeParser",
+]
diff --git a/xsdata/formats/dataclass/parsers/dict.py b/xsdata/formats/dataclass/parsers/dict.py
new file mode 100644
index 00000000..c44423d8
--- /dev/null
+++ b/xsdata/formats/dataclass/parsers/dict.py
@@ -0,0 +1,431 @@
+import warnings
+from dataclasses import dataclass, field
+from typing import Any, Dict, Iterable, List, Optional, Type, Union
+
+from xsdata.exceptions import ConverterWarning, ParserError
+from xsdata.formats.bindings import T
+from xsdata.formats.converter import converter
+from xsdata.formats.dataclass.context import XmlContext
+from xsdata.formats.dataclass.models.elements import XmlMeta, XmlVar
+from xsdata.formats.dataclass.parsers.config import ParserConfig
+from xsdata.formats.dataclass.parsers.utils import ParserUtils
+from xsdata.formats.dataclass.typing import get_args, get_origin
+from xsdata.utils import collections
+from xsdata.utils.constants import EMPTY_MAP
+
+
+@dataclass
+class DictDecoder:
+    """Bind a dictionary or a list of dictionaries to data models.
+
+    Args:
+        config: Parser configuration
+        context: The models context instance
+    """
+
+    config: ParserConfig = field(default_factory=ParserConfig)
+    context: XmlContext = field(default_factory=XmlContext)
+
+    def decode(self, data: Union[List, Dict], clazz: Optional[Type[T]] = None) -> T:
+        """Parse the input stream into the target class type.
+
+        If no clazz is provided, the binding context will try
+        to locate it from imported dataclasses.
+
+        Args:
+            data: A dictionary or list of dictionaries
+            clazz: The target class type to decode the input data
+
+        Returns:
+            An instance of the specified class representing the decoded content.
+        """
+        tp = self.verify_type(clazz, data)
+
+        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)
+
+    def verify_type(self, clazz: Optional[Type[T]], data: Union[Dict, List]) -> Type[T]:
+        """Verify the given data matches the given clazz.
+
+        If no clazz is provided, the binding context will try
+        to locate it from imported dataclasses.
+
+        Args:
+            clazz: The target class type to parse  object
+            data: The loaded dictionary or list of dictionaries
+
+        Returns:
+            The clazz type to bind the loaded data.
+        """
+        if clazz is None:
+            return self.detect_type(data)
+
+        try:
+            origin = get_origin(clazz)
+            list_type = False
+            if origin is list:
+                list_type = True
+                args = get_args(clazz)
+
+                if len(args) != 1 or not self.context.class_type.is_model(args[0]):
+                    raise TypeError()
+
+                clazz = args[0]
+            elif origin is not None:
+                raise TypeError()
+        except TypeError:
+            raise ParserError(f"Invalid clazz argument: {clazz}")
+
+        if list_type != isinstance(data, list):
+            if list_type:
+                raise ParserError("Document is object, expected array")
+            raise ParserError("Document is array, expected object")
+
+        return clazz  # type: ignore
+
+    def detect_type(self, data: Union[Dict, List]) -> Type[T]:
+        """Locate the target clazz type from the data keys.
+
+        Args:
+            data: The loaded dictionary or list of dictionaries
+
+        Returns:
+            The clazz type to bind the loaded data.
+        """
+        if not data:
+            raise ParserError("Document is empty, can not detect type")
+
+        keys = data[0].keys() if isinstance(data, list) else data.keys()
+        clazz: Optional[Type[T]] = self.context.find_type_by_fields(set(keys))
+
+        if clazz:
+            return clazz
+
+        raise ParserError(f"Unable to locate model with properties({list(keys)})")
+
+    def bind_dataclass(self, data: Dict, clazz: Type[T]) -> T:
+        """Create a new instance of the given class type with the given data.
+
+        Args:
+            data: The loaded data
+            clazz: The target class type to bind the input data
+
+        Returns:
+            An instance of the class type representing the parsed content.
+        """
+        if set(data.keys()) == self.context.class_type.derived_keys:
+            return self.bind_derived_dataclass(data, clazz)
+
+        meta = self.context.build(clazz)
+        xml_vars = meta.get_all_vars()
+
+        params = {}
+        for key, value in data.items():
+            is_array = collections.is_array(value)
+            var = self.find_var(xml_vars, key, is_array)
+
+            if var is None and self.config.fail_on_unknown_properties:
+                raise ParserError(f"Unknown property {clazz.__qualname__}.{key}")
+
+            if var and var.init:
+                params[var.name] = self.bind_value(meta, var, value)
+
+        try:
+            return self.config.class_factory(clazz, params)
+        except TypeError as e:
+            raise ParserError(e)
+
+    def bind_derived_dataclass(self, data: Dict, clazz: Type[T]) -> Any:
+        """Bind the input data to the given class type.
+
+        Examples:
+            >>> {
+                "qname": "foo",
+                "type": "my:type",
+                "value": {"prop": "value"}
+            }
+
+        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.
+        """
+        qname = data["qname"]
+        xsi_type = data["type"]
+        params = data["value"]
+
+        generic = self.context.class_type.derived_element
+
+        if clazz is generic:
+            real_clazz: Optional[Type[T]] = None
+            if xsi_type:
+                real_clazz = self.context.find_type(xsi_type)
+
+            if real_clazz is None:
+                raise ParserError(
+                    f"Unable to locate derived model "
+                    f"with properties({list(params.keys())})"
+                )
+
+            value = self.bind_dataclass(params, real_clazz)
+        else:
+            value = self.bind_dataclass(params, clazz)
+
+        return generic(qname=qname, type=xsi_type, value=value)
+
+    def bind_best_dataclass(self, data: Dict, classes: Iterable[Type[T]]) -> T:
+        """Bind the input data to all the given classes and return best match.
+
+        Args:
+            data: The derived element dictionary
+            classes: The target class types to try
+
+        Returns:
+            An instance of one of the class types representing the parsed content.
+        """
+        obj = None
+        keys = set(data.keys())
+        max_score = -1.0
+        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)
+                score = self.context.class_type.score_object(candidate)
+                if score > max_score:
+                    max_score = score
+                    obj = candidate
+
+        if obj:
+            return obj
+
+        raise ParserError(
+            f"Failed to bind object with properties({list(data.keys())}) "
+            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,
+        var: XmlVar,
+        value: Any,
+        recursive: bool = False,
+    ) -> Any:
+        """Main entry point for binding values.
+
+        Args:
+            meta: The parent xml meta instance
+            var: The xml var descriptor for the field
+            value: The data value
+            recursive: Whether this is a recursive call
+
+        Returns:
+            The parsed object
+        """
+        # xs:anyAttributes get it out of the way, it's the mapping exception!
+        if var.is_attributes:
+            return dict(value)
+
+        # Repeating element, recursively bind the values
+        if not recursive and var.list_element and isinstance(value, list):
+            assert var.factory is not None
+            return var.factory(
+                self.bind_value(meta, var, val, recursive=True) for val in value
+            )
+
+        # If not dict this is a text or tokens value.
+        if not isinstance(value, dict):
+            return self.bind_text(meta, var, value)
+
+        keys = value.keys()
+        if keys == self.context.class_type.any_keys:
+            # Bind data to AnyElement dataclass
+            return self.bind_dataclass(value, self.context.class_type.any_element)
+
+        if keys == self.context.class_type.derived_keys:
+            # Bind data to AnyElement dataclass
+            return self.bind_derived_value(meta, var, value)
+
+        # Bind data to a user defined dataclass
+        return self.bind_complex_type(meta, var, value)
+
+    def bind_text(self, meta: XmlMeta, var: XmlVar, value: Any) -> Any:
+        """Bind text/tokens value entrypoint.
+
+        Args:
+            meta: The parent xml meta instance
+            var: The xml var descriptor for the field
+            value: The data value
+
+        Returns:
+            The parsed tokens or text value.
+        """
+        if var.is_elements:
+            # Compound field we need to match the value to one of the choice elements
+            check_subclass = self.context.class_type.is_model(value)
+            choice = var.find_value_choice(value, check_subclass)
+            if choice:
+                return self.bind_text(meta, choice, value)
+
+            if value is None:
+                return value
+
+            raise ParserError(
+                f"Failed to bind '{value}' "
+                f"to {meta.clazz.__qualname__}.{var.name} field"
+            )
+
+        if var.any_type or var.is_wildcard:
+            # field can support any object return the value as it is
+            return value
+
+        value = converter.serialize(value)
+
+        # Convert value according to the field types
+        return ParserUtils.parse_value(
+            value=value,
+            types=var.types,
+            default=var.default,
+            ns_map=EMPTY_MAP,
+            tokens_factory=var.tokens_factory,
+            format=var.format,
+        )
+
+    def bind_complex_type(self, meta: XmlMeta, var: XmlVar, data: Dict) -> Any:
+        """Bind complex values entrypoint.
+
+        Args:
+            meta: The parent xml meta instance
+            var: The xml var descriptor for the field
+            data: The complex data value
+
+        Returns:
+            The parsed dataclass instance.
+        """
+        if var.is_clazz_union:
+            # Union of dataclasses
+            return self.bind_best_dataclass(data, var.types)
+        if var.elements:
+            # Compound field with multiple choices
+            return self.bind_best_dataclass(data, var.element_types)
+        if var.any_type or var.is_wildcard:
+            # xs:anyType element, check all meta classes
+            return self.bind_best_dataclass(data, meta.element_types)
+
+        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)
+
+    def bind_derived_value(self, meta: XmlMeta, var: XmlVar, data: Dict) -> Any:
+        """Bind derived data entrypoint.
+
+        The data is representation of a derived element, e.g. {
+            "qname": "foo",
+            "type": "my:type"
+            "value": Any
+        }
+
+        The data value can be a primitive value or a complex value.
+
+        Args:
+            meta: The parent xml meta instance
+            var: The xml var descriptor for the field
+            data: The derived element data
+
+        Returns:
+            The parsed object.
+        """
+        qname = data["qname"]
+        xsi_type = data["type"]
+        params = data["value"]
+
+        if var.elements:
+            choice = var.find_choice(qname)
+            if choice is None:
+                raise ParserError(
+                    f"Unable to locate compound element"
+                    f" {meta.clazz.__qualname__}.{var.name}[{qname}]"
+                )
+            return self.bind_derived_value(meta, choice, data)
+
+        if not isinstance(params, dict):
+            value = self.bind_text(meta, var, params)
+        elif xsi_type:
+            clazz: Optional[Type] = self.context.find_type(xsi_type)
+
+            if clazz is None:
+                raise ParserError(f"Unable to locate xsi:type `{xsi_type}`")
+
+            value = self.bind_dataclass(params, clazz)
+        elif var.clazz:
+            value = self.bind_complex_type(meta, var, params)
+        else:
+            value = self.bind_best_dataclass(params, meta.element_types)
+
+        generic = self.context.class_type.derived_element
+        return generic(qname=qname, value=value, type=xsi_type)
+
+    @classmethod
+    def find_var(
+        cls,
+        xml_vars: List[XmlVar],
+        local_name: str,
+        is_list: bool = False,
+    ) -> Optional[XmlVar]:
+        """Match the name to a xml variable.
+
+        Args:
+            xml_vars: A list of xml vars
+            local_name: A key from the loaded data
+            is_list: Whether the data value is an array
+
+        Returns:
+            One of the xml vars, if all search attributes match, None otherwise.
+        """
+        for var in xml_vars:
+            if var.local_name == local_name:
+                var_is_list = var.list_element or var.tokens
+                if is_list == var_is_list or var.clazz is None:
+                    return var
+
+        return None
diff --git a/xsdata/formats/dataclass/parsers/json.py b/xsdata/formats/dataclass/parsers/json.py
index 5b5fcecb..a280d9d9 100644
--- a/xsdata/formats/dataclass/parsers/json.py
+++ b/xsdata/formats/dataclass/parsers/json.py
@@ -1,22 +1,13 @@
 import json
-import warnings
 from dataclasses import dataclass, field
-from typing import Any, Callable, Dict, Iterable, List, Optional, Type, Union
+from typing import Any, Callable, Dict, List, Optional, Type, Union
 
-from xsdata.exceptions import ConverterWarning, ParserError
 from xsdata.formats.bindings import AbstractParser, T
-from xsdata.formats.converter import converter
-from xsdata.formats.dataclass.context import XmlContext
-from xsdata.formats.dataclass.models.elements import XmlMeta, XmlVar
-from xsdata.formats.dataclass.parsers.config import ParserConfig
-from xsdata.formats.dataclass.parsers.utils import ParserUtils
-from xsdata.formats.dataclass.typing import get_args, get_origin
-from xsdata.utils import collections
-from xsdata.utils.constants import EMPTY_MAP
+from xsdata.formats.dataclass.parsers import DictDecoder
 
 
 @dataclass
-class JsonParser(AbstractParser):
+class JsonParser(DictDecoder, AbstractParser):
     """Json parser for data classes.
 
     Args:
@@ -25,8 +16,6 @@ class JsonParser(AbstractParser):
         load_factory: Json loader factory
     """
 
-    config: ParserConfig = field(default_factory=ParserConfig)
-    context: XmlContext = field(default_factory=XmlContext)
     load_factory: Callable = field(default=json.load)
 
     def parse(self, source: Any, clazz: Optional[Type[T]] = None) -> T:
@@ -43,19 +32,7 @@ class JsonParser(AbstractParser):
             An instance of the specified class representing the parsed content.
         """
         data = self.load_json(source)
-        tp = self.verify_type(clazz, data)
-
-        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.decode(data, clazz)
 
     def load_json(self, source: Any) -> Union[Dict, List]:
         """Load the given json source filename or stream.
@@ -71,401 +48,3 @@ class JsonParser(AbstractParser):
                 return self.load_factory(fp)
 
         return self.load_factory(source)
-
-    def verify_type(self, clazz: Optional[Type[T]], data: Union[Dict, List]) -> Type[T]:
-        """Verify the given data matches the given clazz.
-
-        If no clazz is provided, the binding context will try
-        to locate it from imported dataclasses.
-
-        Args:
-            clazz: The target class type to parse  object
-            data: The loaded dictionary or list of dictionaries
-
-        Returns:
-            The clazz type to bind the loaded data.
-        """
-        if clazz is None:
-            return self.detect_type(data)
-
-        try:
-            origin = get_origin(clazz)
-            list_type = False
-            if origin is list:
-                list_type = True
-                args = get_args(clazz)
-
-                if len(args) != 1 or not self.context.class_type.is_model(args[0]):
-                    raise TypeError()
-
-                clazz = args[0]
-            elif origin is not None:
-                raise TypeError()
-        except TypeError:
-            raise ParserError(f"Invalid clazz argument: {clazz}")
-
-        if list_type != isinstance(data, list):
-            if list_type:
-                raise ParserError("Document is object, expected array")
-            raise ParserError("Document is array, expected object")
-
-        return clazz  # type: ignore
-
-    def detect_type(self, data: Union[Dict, List]) -> Type[T]:
-        """Locate the target clazz type from the data keys.
-
-        Args:
-            data: The loaded dictionary or list of dictionaries
-
-        Returns:
-            The clazz type to bind the loaded data.
-        """
-        if not data:
-            raise ParserError("Document is empty, can not detect type")
-
-        keys = data[0].keys() if isinstance(data, list) else data.keys()
-        clazz: Optional[Type[T]] = self.context.find_type_by_fields(set(keys))
-
-        if clazz:
-            return clazz
-
-        raise ParserError(f"Unable to locate model with properties({list(keys)})")
-
-    def bind_dataclass(self, data: Dict, clazz: Type[T]) -> T:
-        """Create a new instance of the given class type with the given data.
-
-        Args:
-            data: The loaded data
-            clazz: The target class type to bind the input data
-
-        Returns:
-            An instance of the class type representing the parsed content.
-        """
-        if set(data.keys()) == self.context.class_type.derived_keys:
-            return self.bind_derived_dataclass(data, clazz)
-
-        meta = self.context.build(clazz)
-        xml_vars = meta.get_all_vars()
-
-        params = {}
-        for key, value in data.items():
-            is_array = collections.is_array(value)
-            var = self.find_var(xml_vars, key, is_array)
-
-            if var is None and self.config.fail_on_unknown_properties:
-                raise ParserError(f"Unknown property {clazz.__qualname__}.{key}")
-
-            if var and var.init:
-                params[var.name] = self.bind_value(meta, var, value)
-
-        try:
-            return self.config.class_factory(clazz, params)
-        except TypeError as e:
-            raise ParserError(e)
-
-    def bind_derived_dataclass(self, data: Dict, clazz: Type[T]) -> Any:
-        """Bind the input data to the given class type.
-
-        Examples:
-            >>> {
-                "qname": "foo",
-                "type": "my:type",
-                "value": {"prop": "value"}
-            }
-
-        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.
-        """
-        qname = data["qname"]
-        xsi_type = data["type"]
-        params = data["value"]
-
-        generic = self.context.class_type.derived_element
-
-        if clazz is generic:
-            real_clazz: Optional[Type[T]] = None
-            if xsi_type:
-                real_clazz = self.context.find_type(xsi_type)
-
-            if real_clazz is None:
-                raise ParserError(
-                    f"Unable to locate derived model "
-                    f"with properties({list(params.keys())})"
-                )
-
-            value = self.bind_dataclass(params, real_clazz)
-        else:
-            value = self.bind_dataclass(params, clazz)
-
-        return generic(qname=qname, type=xsi_type, value=value)
-
-    def bind_best_dataclass(self, data: Dict, classes: Iterable[Type[T]]) -> T:
-        """Bind the input data to all the given classes and return best match.
-
-        Args:
-            data: The derived element dictionary
-            classes: The target class types to try
-
-        Returns:
-            An instance of one of the class types representing the parsed content.
-        """
-        obj = None
-        keys = set(data.keys())
-        max_score = -1.0
-        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)
-                score = self.context.class_type.score_object(candidate)
-                if score > max_score:
-                    max_score = score
-                    obj = candidate
-
-        if obj:
-            return obj
-
-        raise ParserError(
-            f"Failed to bind object with properties({list(data.keys())}) "
-            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,
-        var: XmlVar,
-        value: Any,
-        recursive: bool = False,
-    ) -> Any:
-        """Main entry point for binding values.
-
-        Args:
-            meta: The parent xml meta instance
-            var: The xml var descriptor for the field
-            value: The data value
-            recursive: Whether this is a recursive call
-
-        Returns:
-            The parsed object
-        """
-        # xs:anyAttributes get it out of the way, it's the mapping exception!
-        if var.is_attributes:
-            return dict(value)
-
-        # Repeating element, recursively bind the values
-        if not recursive and var.list_element and isinstance(value, list):
-            assert var.factory is not None
-            return var.factory(
-                self.bind_value(meta, var, val, recursive=True) for val in value
-            )
-
-        # If not dict this is a text or tokens value.
-        if not isinstance(value, dict):
-            return self.bind_text(meta, var, value)
-
-        keys = value.keys()
-        if keys == self.context.class_type.any_keys:
-            # Bind data to AnyElement dataclass
-            return self.bind_dataclass(value, self.context.class_type.any_element)
-
-        if keys == self.context.class_type.derived_keys:
-            # Bind data to AnyElement dataclass
-            return self.bind_derived_value(meta, var, value)
-
-        # Bind data to a user defined dataclass
-        return self.bind_complex_type(meta, var, value)
-
-    def bind_text(self, meta: XmlMeta, var: XmlVar, value: Any) -> Any:
-        """Bind text/tokens value entrypoint.
-
-        Args:
-            meta: The parent xml meta instance
-            var: The xml var descriptor for the field
-            value: The data value
-
-        Returns:
-            The parsed tokens or text value.
-        """
-        if var.is_elements:
-            # Compound field we need to match the value to one of the choice elements
-            check_subclass = self.context.class_type.is_model(value)
-            choice = var.find_value_choice(value, check_subclass)
-            if choice:
-                return self.bind_text(meta, choice, value)
-
-            if value is None:
-                return value
-
-            raise ParserError(
-                f"Failed to bind '{value}' "
-                f"to {meta.clazz.__qualname__}.{var.name} field"
-            )
-
-        if var.any_type or var.is_wildcard:
-            # field can support any object return the value as it is
-            return value
-
-        value = converter.serialize(value)
-
-        # Convert value according to the field types
-        return ParserUtils.parse_value(
-            value=value,
-            types=var.types,
-            default=var.default,
-            ns_map=EMPTY_MAP,
-            tokens_factory=var.tokens_factory,
-            format=var.format,
-        )
-
-    def bind_complex_type(self, meta: XmlMeta, var: XmlVar, data: Dict) -> Any:
-        """Bind complex values entrypoint.
-
-        Args:
-            meta: The parent xml meta instance
-            var: The xml var descriptor for the field
-            data: The complex data value
-
-        Returns:
-            The parsed dataclass instance.
-        """
-        if var.is_clazz_union:
-            # Union of dataclasses
-            return self.bind_best_dataclass(data, var.types)
-        if var.elements:
-            # Compound field with multiple choices
-            return self.bind_best_dataclass(data, var.element_types)
-        if var.any_type or var.is_wildcard:
-            # xs:anyType element, check all meta classes
-            return self.bind_best_dataclass(data, meta.element_types)
-
-        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)
-
-    def bind_derived_value(self, meta: XmlMeta, var: XmlVar, data: Dict) -> Any:
-        """Bind derived data entrypoint.
-
-        The data is representation of a derived element, e.g. {
-            "qname": "foo",
-            "type": "my:type"
-            "value": Any
-        }
-
-        The data value can be a primitive value or a complex value.
-
-        Args:
-            meta: The parent xml meta instance
-            var: The xml var descriptor for the field
-            data: The derived element data
-
-        Returns:
-            The parsed object.
-        """
-        qname = data["qname"]
-        xsi_type = data["type"]
-        params = data["value"]
-
-        if var.elements:
-            choice = var.find_choice(qname)
-            if choice is None:
-                raise ParserError(
-                    f"Unable to locate compound element"
-                    f" {meta.clazz.__qualname__}.{var.name}[{qname}]"
-                )
-            return self.bind_derived_value(meta, choice, data)
-
-        if not isinstance(params, dict):
-            value = self.bind_text(meta, var, params)
-        elif xsi_type:
-            clazz: Optional[Type] = self.context.find_type(xsi_type)
-
-            if clazz is None:
-                raise ParserError(f"Unable to locate xsi:type `{xsi_type}`")
-
-            value = self.bind_dataclass(params, clazz)
-        elif var.clazz:
-            value = self.bind_complex_type(meta, var, params)
-        else:
-            value = self.bind_best_dataclass(params, meta.element_types)
-
-        generic = self.context.class_type.derived_element
-        return generic(qname=qname, value=value, type=xsi_type)
-
-    @classmethod
-    def find_var(
-        cls,
-        xml_vars: List[XmlVar],
-        local_name: str,
-        is_list: bool = False,
-    ) -> Optional[XmlVar]:
-        """Match the name to a xml variable.
-
-        Args:
-            xml_vars: A list of xml vars
-            local_name: A key from the loaded data
-            is_list: Whether the data value is an array
-
-        Returns:
-            One of the xml vars, if all search attributes match, None otherwise.
-        """
-        for var in xml_vars:
-            if var.local_name == local_name:
-                var_is_list = var.list_element or var.tokens
-                if is_list == var_is_list or var.clazz is None:
-                    return var
-
-        return None
-
-
-@dataclass
-class DictConverter(JsonParser):
-    """Map data to a data class.
-
-    This is not a parser technically, as it doesn't
-    implement the parser interface correctly.
-    """
-
-    def convert(self, data: Dict[str, Any], clazz: Type[T]) -> T:
-        """Parse the input data into the target class type.
-
-        Args:
-            data: The input dictionary
-            clazz: The target class type to parse the input data
-
-        Returns:
-            An instance of the specified class representing the parsed data.
-        """
-        return self.bind_dataclass(data, clazz)
diff --git a/xsdata/formats/dataclass/serializers/__init__.py b/xsdata/formats/dataclass/serializers/__init__.py
index 839a0098..42f98277 100644
--- a/xsdata/formats/dataclass/serializers/__init__.py
+++ b/xsdata/formats/dataclass/serializers/__init__.py
@@ -1,8 +1,10 @@
 from xsdata.formats.dataclass.serializers.code import PycodeSerializer
+from xsdata.formats.dataclass.serializers.dict import DictEncoder
 from xsdata.formats.dataclass.serializers.json import DictFactory, JsonSerializer
 from xsdata.formats.dataclass.serializers.xml import XmlSerializer
 
 __all__ = [
+    "DictEncoder",
     "DictFactory",
     "JsonSerializer",
     "XmlSerializer",
diff --git a/xsdata/formats/dataclass/serializers/dict.py b/xsdata/formats/dataclass/serializers/dict.py
new file mode 100644
index 00000000..743a71b9
--- /dev/null
+++ b/xsdata/formats/dataclass/serializers/dict.py
@@ -0,0 +1,90 @@
+from dataclasses import dataclass, field
+from enum import Enum
+from typing import Any, Callable, Dict, Iterator, Optional, Tuple
+
+from xsdata.formats.converter import converter
+from xsdata.formats.dataclass.context import XmlContext
+from xsdata.formats.dataclass.models.elements import XmlVar
+from xsdata.formats.dataclass.serializers.config import SerializerConfig
+from xsdata.utils import collections
+
+
+def filter_none(x: Tuple) -> Dict:
+    """Convert a key-value pairs to dict, ignoring None values.
+
+    Args:
+        x: Key-value pairs
+
+    Returns:
+        The filtered dictionary.
+    """
+    return {k: v for k, v in x if v is not None}
+
+
+class DictFactory:
+    """Dictionary factory types."""
+
+    FILTER_NONE = filter_none
+
+
+@dataclass
+class DictEncoder:
+    """Json serializer for data classes.
+
+    Args:
+        config: The serializer config instance
+        context: The models context instance
+        dict_factory: Dictionary factory
+    """
+
+    config: SerializerConfig = field(default_factory=SerializerConfig)
+    context: XmlContext = field(default_factory=XmlContext)
+    dict_factory: Callable = field(default=dict)
+
+    def encode(self, value: Any, var: Optional[XmlVar] = None) -> Any:
+        """Convert a value to a dictionary object.
+
+        Args:
+            value: The input value
+            var: The xml var instance
+
+        Returns:
+            The converted json serializable value.
+        """
+        if var is None or self.context.class_type.is_model(value):
+            if collections.is_array(value):
+                return list(map(self.encode, value))
+
+            return self.dict_factory(self.next_value(value))
+
+        if collections.is_array(value):
+            return type(value)(self.encode(val, var) for val in value)
+
+        if isinstance(value, (dict, int, float, str, bool)):
+            return value
+
+        if isinstance(value, Enum):
+            return self.encode(value.value, var)
+
+        return converter.serialize(value, format=var.format)
+
+    def next_value(self, obj: Any) -> Iterator[Tuple[str, Any]]:
+        """Fetch the next value of a model instance to convert.
+
+        Args:
+            obj: The input model instance
+
+        Yields:
+            An iterator of field name and value tuples.
+        """
+        ignore_optionals = self.config.ignore_default_attributes
+        meta = self.context.build(obj.__class__, globalns=self.config.globalns)
+
+        for var in meta.get_all_vars():
+            value = getattr(obj, var.name)
+            if (
+                not var.is_attribute
+                or not ignore_optionals
+                or not var.is_optional(value)
+            ):
+                yield var.local_name, self.encode(value, var)
diff --git a/xsdata/formats/dataclass/serializers/json.py b/xsdata/formats/dataclass/serializers/json.py
index 8a355f26..624394e5 100644
--- a/xsdata/formats/dataclass/serializers/json.py
+++ b/xsdata/formats/dataclass/serializers/json.py
@@ -1,15 +1,10 @@
 import json
 from dataclasses import dataclass, field
-from enum import Enum
 from io import StringIO
-from typing import Any, Callable, Dict, Iterator, Optional, TextIO, Tuple, Union
+from typing import Any, Callable, Dict, Optional, TextIO, Tuple, Union
 
 from xsdata.formats.bindings import AbstractSerializer
-from xsdata.formats.converter import converter
-from xsdata.formats.dataclass.context import XmlContext
-from xsdata.formats.dataclass.models.elements import XmlVar
-from xsdata.formats.dataclass.serializers.config import SerializerConfig
-from xsdata.utils import collections
+from xsdata.formats.dataclass.serializers import DictEncoder
 
 
 def filter_none(x: Tuple) -> Dict:
@@ -31,7 +26,7 @@ class DictFactory:
 
 
 @dataclass
-class JsonSerializer(AbstractSerializer):
+class JsonSerializer(DictEncoder, AbstractSerializer):
     """Json serializer for data classes.
 
     Args:
@@ -41,9 +36,6 @@ class JsonSerializer(AbstractSerializer):
         dump_factory: Json dump factory e.g. json.dump
     """
 
-    config: SerializerConfig = field(default_factory=SerializerConfig)
-    context: XmlContext = field(default_factory=XmlContext)
-    dict_factory: Callable = field(default=dict)
     dump_factory: Callable = field(default=json.dump)
 
     def render(self, obj: Any) -> str:
@@ -70,52 +62,4 @@ class JsonSerializer(AbstractSerializer):
         if self.config.pretty_print:
             indent = self.config.pretty_print_indent or 2
 
-        self.dump_factory(self.convert(obj), out, indent=indent)
-
-    def convert(self, value: Any, var: Optional[XmlVar] = None) -> Any:
-        """Convert a value to json serializable object.
-
-        Args:
-            value: The input value
-            var: The xml var instance
-
-        Returns:
-            The converted json serializable value.
-        """
-        if var is None or self.context.class_type.is_model(value):
-            if collections.is_array(value):
-                return list(map(self.convert, value))
-
-            return self.dict_factory(self.next_value(value))
-
-        if collections.is_array(value):
-            return type(value)(self.convert(val, var) for val in value)
-
-        if isinstance(value, (dict, int, float, str, bool)):
-            return value
-
-        if isinstance(value, Enum):
-            return self.convert(value.value, var)
-
-        return converter.serialize(value, format=var.format)
-
-    def next_value(self, obj: Any) -> Iterator[Tuple[str, Any]]:
-        """Fetch the next value of a model instance to convert.
-
-        Args:
-            obj: The input model instance
-
-        Yields:
-            An iterator of field name and value tuples.
-        """
-        ignore_optionals = self.config.ignore_default_attributes
-        meta = self.context.build(obj.__class__, globalns=self.config.globalns)
-
-        for var in meta.get_all_vars():
-            value = getattr(obj, var.name)
-            if (
-                not var.is_attribute
-                or not ignore_optionals
-                or not var.is_optional(value)
-            ):
-                yield var.local_name, self.convert(value, var)
+        self.dump_factory(self.encode(obj), out, indent=indent)


Test patch:
diff --git a/tests/formats/dataclass/parsers/test_dict.py b/tests/formats/dataclass/parsers/test_dict.py
new file mode 100644
index 00000000..77b1e164
--- /dev/null
+++ b/tests/formats/dataclass/parsers/test_dict.py
@@ -0,0 +1,378 @@
+import json
+from dataclasses import asdict, make_dataclass
+from typing import List, Optional, Union
+from xml.etree.ElementTree import QName
+
+from tests import fixtures_dir
+from tests.fixtures.books import BookForm, Books
+from tests.fixtures.models import (
+    AttrsType,
+    BaseC,
+    BaseType,
+    ChoiceType,
+    ExtendedType,
+    OptionalChoiceType,
+    TypeA,
+    TypeB,
+    TypeC,
+    TypeD,
+    UnionType,
+)
+from xsdata.exceptions import ParserError
+from xsdata.formats.dataclass.models.generics import AnyElement, DerivedElement
+from xsdata.formats.dataclass.parsers import DictDecoder
+from xsdata.models.datatype import XmlDate
+from xsdata.utils.testing import FactoryTestCase
+
+
+class DictDecoderTests(FactoryTestCase):
+    def setUp(self) -> None:
+        super().setUp()
+
+        self.decoder = DictDecoder()
+
+    def test_decode(self):
+        path = fixtures_dir.joinpath("books/books.json")
+        source = json.loads(path.read_text())
+        books = self.decoder.decode(source, Books)
+
+        self.assertIsInstance(books, Books)
+
+        self.assertEqual(
+            BookForm(
+                author="Hightower, Kim",
+                title="The First Book",
+                genre="Fiction",
+                price=44.95,
+                pub_date=XmlDate.from_string("2000-10-01"),
+                review="An amazing story of nothing.",
+                id="bk001",
+            ),
+            books.book[0],
+        )
+
+        self.assertEqual(
+            BookForm(
+                author="Nagata, Suanne",
+                title="Becoming Somebody",
+                genre="Biography",
+                price=None,
+                pub_date=None,
+                review="A masterpiece of the fine art of gossiping.",
+                id="bk002",
+            ),
+            books.book[1],
+        )
+
+    def test_decode_empty_document(self):
+        self.assertEqual(BookForm(), self.decoder.decode({}, BookForm))
+        self.assertEqual([], self.decoder.decode([], List[BookForm]))
+
+    def test_decode_list_of_objects(self):
+        path = fixtures_dir.joinpath("books/books.json")
+        data = json.loads(path.read_text())
+        book_list = data["book"]
+
+        books = self.decoder.decode(book_list, List[BookForm])
+        self.assertIsInstance(books, list)
+        self.assertEqual(2, len(books))
+        self.assertIsInstance(books[0], BookForm)
+        self.assertIsInstance(books[1], BookForm)
+
+    def test_decode_with_unknown_class(self):
+        path = fixtures_dir.joinpath("books/books.json")
+        source = json.loads(path.read_text())
+        books = self.decoder.decode(source)
+        self.assertIsInstance(books, Books)
+        self.assertEqual(2, len(books.book))
+
+        book_list = self.decoder.decode(source["book"])
+        self.assertIsInstance(book_list, list)
+        self.assertEqual(2, len(book_list))
+        self.assertIsInstance(book_list[0], BookForm)
+        self.assertIsInstance(book_list[1], BookForm)
+
+    def test_decode_with_fail_on_converter_warnings(self):
+        json_str = {"x": "foo"}
+        self.decoder.config.fail_on_converter_warnings = True
+        with self.assertRaises(ParserError) as cm:
+            self.decoder.decode(json_str, TypeA)
+
+        self.assertEqual(
+            "Failed to convert value `foo` to one of (<class 'int'>,)",
+            str(cm.exception),
+        )
+
+    def test_verify_type(self):
+        invalid_cases = [
+            (
+                {"not": 1, "found": 1},
+                None,
+                "Unable to locate model with properties(['not', 'found'])",
+            ),
+            ({}, None, "Document is empty, can not detect type"),
+            ([], BookForm, "Document is array, expected object"),
+            ({}, List[BookForm], "Document is object, expected array"),
+            (
+                {},
+                Optional[ChoiceType],
+                f"Invalid clazz argument: {Optional[ChoiceType]}",
+            ),
+            ([], List[int], f"Invalid clazz argument: {List[int]}"),
+            ([], List, f"Invalid clazz argument: {List}"),
+        ]
+
+        for source, clazz, exc_msg in invalid_cases:
+            with self.assertRaises(ParserError) as cm:
+                self.decoder.decode(source, clazz=clazz)
+
+            self.assertEqual(exc_msg, str(cm.exception))
+
+    def test_bind_dataclass_with_unknown_property(self):
+        data = {"unknown": True}
+        with self.assertRaises(ParserError) as cm:
+            self.decoder.bind_dataclass(data, Books)
+
+        self.assertEqual("Unknown property Books.unknown", str(cm.exception))
+
+        self.decoder.config.fail_on_unknown_properties = False
+        self.assertEqual(Books(), self.decoder.bind_dataclass(data, Books))
+
+    def test_bind_dataclass_with_required_fields(self):
+        obj = self.decoder.bind_dataclass({"x": 1, "y": "a", "z": None}, TypeD)
+
+        self.assertEqual(1, obj.x)
+        self.assertEqual("a", obj.y)
+        self.assertIsNone(obj.z)
+
+        with self.assertRaises(ParserError):
+            self.decoder.bind_dataclass({"x": 1, "y": "a"}, TypeD)
+
+    def test_bind_derived_dataclass(self):
+        data = {
+            "qname": "{urn:books}BookForm",
+            "type": None,
+            "value": {
+                "author": "Nagata, Suanne",
+                "title": "Becoming Somebody",
+            },
+        }
+
+        actual = self.decoder.bind_dataclass(data, BookForm)
+        expected = DerivedElement(
+            qname="{urn:books}BookForm",
+            value=BookForm(author="Nagata, Suanne", title="Becoming Somebody"),
+            type=None,
+        )
+        self.assertEqual(expected, actual)
+
+    def test_bind_derived_dataclass_with_xsi_type(self):
+        data = {
+            "qname": "foobar",
+            "type": "{urn:books}BookForm",
+            "value": {
+                "author": "Nagata, Suanne",
+                "title": "Becoming Somebody",
+            },
+        }
+
+        actual = self.decoder.bind_dataclass(data, DerivedElement)
+        expected = DerivedElement(
+            qname="foobar",
+            value=BookForm(author="Nagata, Suanne", title="Becoming Somebody"),
+            type="{urn:books}BookForm",
+        )
+        self.assertEqual(expected, actual)
+
+        with self.assertRaises(ParserError) as cm:
+            data["type"] = "notExists"
+            self.decoder.bind_dataclass(data, DerivedElement)
+
+        self.assertEqual(
+            "Unable to locate derived model with" " properties(['author', 'title'])",
+            str(cm.exception),
+        )
+
+        with self.assertRaises(ParserError) as cm:
+            data["type"] = None
+            self.decoder.bind_dataclass(data, DerivedElement)
+
+    def test_bind_dataclass_union(self):
+        data = {"element": {"x": 1, "y": "foo", "z": "1.0"}}
+        actual = self.decoder.bind_dataclass(data, UnionType)
+
+        self.assertIsInstance(actual.element, TypeC)
+
+        with self.assertRaises(ParserError) as cm:
+            self.decoder.bind_dataclass({"element": {"a": 1}}, UnionType)
+
+        self.assertEqual(
+            "Failed to bind object with properties(['a']) "
+            "to any of the ['TypeA', 'TypeB', 'TypeC', 'TypeD']",
+            str(cm.exception),
+        )
+
+    def test_bind_dataclass_subclasses(self):
+        data = {"element": {"x": "1", "y": "foo", "z": "1.0"}}
+        actual = self.decoder.bind_dataclass(data, BaseType)
+
+        self.assertIsInstance(actual.element, BaseC)
+
+    def test_bind_attributes(self):
+        data = {"attrs": {"a": 1, "b": 2}, "index": 1}
+
+        actual = self.decoder.bind_dataclass(data, AttrsType)
+        self.assertEqual(data["attrs"], actual.attrs)
+        self.assertIsNot(data["attrs"], actual.attrs)
+
+    def test_bind_simple_type_with_wildcard_var(self):
+        data = {"any": 1, "wildcard": 2}
+        actual = self.decoder.bind_dataclass(data, ExtendedType)
+        self.assertEqual(1, actual.any)
+        self.assertEqual(2, actual.wildcard)
+
+    def test_bind_simple_type_with_elements_var(self):
+        data = {"choice": ["1.0", 1, ["1"], "a", "{a}b"]}
+
+        actual = self.decoder.bind_dataclass(data, ChoiceType)
+
+        self.assertEqual(1.0, actual.choice[0])
+        self.assertEqual(1, actual.choice[1])
+        self.assertEqual([1], actual.choice[2])
+        self.assertEqual(QName("a"), actual.choice[3])
+        self.assertIsInstance(actual.choice[3], QName)
+        self.assertEqual(QName("{a}b"), actual.choice[4])
+        self.assertIsInstance(actual.choice[4], QName)
+
+        data = {"choice": ["!NotAQname"]}
+        with self.assertRaises(ParserError) as cm:
+            self.decoder.bind_dataclass(data, ChoiceType)
+
+        self.assertEqual(
+            "Failed to bind '!NotAQname' to ChoiceType.choice field",
+            str(cm.exception),
+        )
+
+    def test_bind_simple_type_with_optional_elements_var(self):
+        data = {"a_or_b": None}
+        actual = self.decoder.bind_dataclass(data, OptionalChoiceType)
+        self.assertIsNone(None, actual.a_or_b)
+
+    def test_bind_any_element(self):
+        data = {
+            "wildcard": {
+                "qname": "a",
+                "text": 1,
+                "tail": None,
+                "children": [],
+                "attributes": {},
+            }
+        }
+        self.assertEqual(
+            ExtendedType(wildcard=AnyElement(qname="a", text="1")),
+            self.decoder.bind_dataclass(data, ExtendedType),
+        )
+
+    def test_bind_choice_dataclass(self):
+        data = {"choice": [{"x": 1}, {"x": 1, "y": "a"}]}
+        expected = ChoiceType(choice=[TypeA(x=1), TypeB(x=1, y="a")])
+        self.assertEqual(expected, self.decoder.bind_dataclass(data, ChoiceType))
+
+    def test_bind_derived_value_with_simple_type(self):
+        data = {"choice": [{"qname": "int2", "value": 1, "type": None}]}
+
+        actual = self.decoder.bind_dataclass(data, ChoiceType)
+        expected = ChoiceType(choice=[DerivedElement(qname="int2", value=1)])
+        self.assertEqual(expected, actual)
+
+    def test_bind_derived_value_with_choice_var(self):
+        data = {
+            "choice": [
+                {
+                    "qname": "b",
+                    "type": None,
+                    "value": {
+                        "x": "1",
+                        "y": "a",
+                    },
+                }
+            ]
+        }
+        expected = ChoiceType(
+            choice=[
+                DerivedElement(
+                    qname="b",
+                    value=TypeB(x=1, y="a"),
+                )
+            ]
+        )
+        self.assertEqual(expected, self.decoder.bind_dataclass(data, ChoiceType))
+
+        with self.assertRaises(ParserError) as cm:
+            data["choice"][0]["qname"] = "nope"
+            self.decoder.bind_dataclass(data, ChoiceType)
+
+        self.assertEqual(
+            "Unable to locate compound element ChoiceType.choice[nope]",
+            str(cm.exception),
+        )
+
+    def test_bind_wildcard_dataclass(self):
+        data = {"a": None, "wildcard": {"x": 1}}
+        expected = ExtendedType(wildcard=TypeA(x=1))
+        self.assertEqual(expected, self.decoder.bind_dataclass(data, ExtendedType))
+
+    def test_bind_wildcard_with_derived_dataclass(self):
+        data = {
+            "wildcard": {
+                "qname": "b",
+                "type": "{xsdata}TypeB",
+                "value": {
+                    "x": "1",
+                    "y": "a",
+                },
+            }
+        }
+        expected = ExtendedType(
+            wildcard=DerivedElement(
+                qname="b", value=TypeB(x=1, y="a"), type="{xsdata}TypeB"
+            )
+        )
+        self.assertEqual(expected, self.decoder.bind_dataclass(data, ExtendedType))
+
+    def test_bind_any_type_with_derived_dataclass(self):
+        data = {
+            "any": {
+                "qname": "any",
+                "type": None,
+                "value": {"x": "1"},
+            }
+        }
+        expected = ExtendedType(any=DerivedElement(qname="any", value=TypeA(x=1)))
+        self.assertEqual(expected, self.decoder.bind_dataclass(data, ExtendedType))
+
+        with self.assertRaises(ParserError) as cm:
+            data["any"]["type"] = "notexists"
+            self.decoder.bind_dataclass(data, ExtendedType)
+
+        self.assertEqual("Unable to locate xsi:type `notexists`", str(cm.exception))
+
+    def test_bind_text_with_unions(self):
+        Fixture = make_dataclass("Fixture", [("x", List[Union[int, float, str, bool]])])
+        values = ["foo", 12.2, "12.2", 12, "12", True, "false"]
+
+        result = self.decoder.decode({"x": values}, Fixture)
+        expected = ["foo", 12.2, 12.2, 12, 12, True, False]
+        self.assertEqual({"x": expected}, asdict(result))
+
+    def test_find_var(self):
+        meta = self.decoder.context.build(TypeB)
+        xml_vars = meta.get_all_vars()
+
+        self.assertEqual(xml_vars[0], self.decoder.find_var(xml_vars, "x"))
+        self.assertEqual(xml_vars[0], self.decoder.find_var(xml_vars, "x", True))
+
+        meta = self.decoder.context.build(ExtendedType)
+        xml_vars = meta.get_all_vars()
+        self.assertIsNone(self.decoder.find_var(xml_vars, "a", True))
+        self.assertEqual(xml_vars[0], self.decoder.find_var(xml_vars, "a"))
diff --git a/tests/formats/dataclass/parsers/test_json.py b/tests/formats/dataclass/parsers/test_json.py
index 1b99aca5..9dee1ced 100644
--- a/tests/formats/dataclass/parsers/test_json.py
+++ b/tests/formats/dataclass/parsers/test_json.py
@@ -1,28 +1,6 @@
-import json
-from dataclasses import asdict, make_dataclass
-from typing import List, Optional, Union
-from xml.etree.ElementTree import QName
-
 from tests import fixtures_dir
-from tests.fixtures.books import BookForm, Books
-from tests.fixtures.models import (
-    AttrsType,
-    BaseC,
-    BaseType,
-    ChoiceType,
-    ExtendedType,
-    OptionalChoiceType,
-    TypeA,
-    TypeB,
-    TypeC,
-    TypeD,
-    UnionType,
-)
-from xsdata.exceptions import ParserError
-from xsdata.formats.dataclass.models.generics import AnyElement, DerivedElement
+from tests.fixtures.books import Books
 from xsdata.formats.dataclass.parsers.json import JsonParser
-from xsdata.formats.dataclass.serializers import JsonSerializer
-from xsdata.models.datatype import XmlDate
 from xsdata.utils.testing import FactoryTestCase
 
 
@@ -32,57 +10,12 @@ class JsonParserTests(FactoryTestCase):
 
         self.parser = JsonParser()
 
-    def test_parser(self):
+    def test_parser_entry_points(self):
         path = fixtures_dir.joinpath("books/books.json")
-        books = self.parser.from_path(path, Books)
 
+        books = self.parser.from_path(path)
         self.assertIsInstance(books, Books)
 
-        self.assertEqual(
-            BookForm(
-                author="Hightower, Kim",
-                title="The First Book",
-                genre="Fiction",
-                price=44.95,
-                pub_date=XmlDate.from_string("2000-10-01"),
-                review="An amazing story of nothing.",
-                id="bk001",
-            ),
-            books.book[0],
-        )
-
-        self.assertEqual(
-            BookForm(
-                author="Nagata, Suanne",
-                title="Becoming Somebody",
-                genre="Biography",
-                price=None,
-                pub_date=None,
-                review="A masterpiece of the fine art of gossiping.",
-                id="bk002",
-            ),
-            books.book[1],
-        )
-
-    def test_parse_empty_document(self):
-        self.assertEqual(BookForm(), self.parser.from_string("{}", BookForm))
-        self.assertEqual([], self.parser.from_string("[]", List[BookForm]))
-
-    def test_parse_list_of_objects(self):
-        path = fixtures_dir.joinpath("books/books.json")
-        data = json.loads(path.read_text())
-        book_list = data["book"]
-        json_string = json.dumps(book_list)
-
-        books = self.parser.from_string(json_string, List[BookForm])
-        self.assertIsInstance(books, list)
-        self.assertEqual(2, len(books))
-        self.assertIsInstance(books[0], BookForm)
-        self.assertIsInstance(books[1], BookForm)
-
-    def test_parser_entry_points(self):
-        path = fixtures_dir.joinpath("books/books.json")
-
         books = self.parser.from_string(path.read_text(), Books)
         self.assertIsInstance(books, Books)
 
@@ -91,302 +24,3 @@ class JsonParserTests(FactoryTestCase):
 
         books = self.parser.parse(str(path), Books)
         self.assertIsInstance(books, Books)
-
-    def test_parse_with_unknown_class(self):
-        path = fixtures_dir.joinpath("books/books.json")
-        books = self.parser.from_path(path)
-        self.assertIsInstance(books, Books)
-        self.assertEqual(2, len(books.book))
-
-        json_array = JsonSerializer().render(books.book)
-        book_list = self.parser.from_string(json_array)
-        self.assertIsInstance(book_list, list)
-        self.assertEqual(2, len(book_list))
-        self.assertIsInstance(book_list[0], BookForm)
-        self.assertIsInstance(book_list[1], BookForm)
-
-    def test_parse_with_fail_on_converter_warnings(self):
-        json_str = '{"x": "foo"}'
-        self.parser.config.fail_on_converter_warnings = True
-        with self.assertRaises(ParserError) as cm:
-            self.parser.from_string(json_str, TypeA)
-
-        self.assertEqual(
-            "Failed to convert value `foo` to one of (<class 'int'>,)",
-            str(cm.exception),
-        )
-
-    def test_verify_type(self):
-        invalid_cases = [
-            (
-                '{"not": 1, "found": 1}',
-                None,
-                "Unable to locate model with properties(['not', 'found'])",
-            ),
-            ("{}", None, "Document is empty, can not detect type"),
-            ("[]", BookForm, "Document is array, expected object"),
-            ("{}", List[BookForm], "Document is object, expected array"),
-            (
-                "{}",
-                Optional[ChoiceType],
-                f"Invalid clazz argument: {Optional[ChoiceType]}",
-            ),
-            ("[]", List[int], f"Invalid clazz argument: {List[int]}"),
-            ("[]", List, f"Invalid clazz argument: {List}"),
-        ]
-
-        for json_string, clazz, exc_msg in invalid_cases:
-            with self.assertRaises(ParserError) as cm:
-                self.parser.from_string(json_string, clazz=clazz)
-
-            self.assertEqual(exc_msg, str(cm.exception))
-
-    def test_bind_dataclass_with_unknown_property(self):
-        data = {"unknown": True}
-        with self.assertRaises(ParserError) as cm:
-            self.parser.bind_dataclass(data, Books)
-
-        self.assertEqual("Unknown property Books.unknown", str(cm.exception))
-
-        self.parser.config.fail_on_unknown_properties = False
-        self.assertEqual(Books(), self.parser.bind_dataclass(data, Books))
-
-    def test_bind_dataclass_with_required_fields(self):
-        obj = self.parser.bind_dataclass({"x": 1, "y": "a", "z": None}, TypeD)
-
-        self.assertEqual(1, obj.x)
-        self.assertEqual("a", obj.y)
-        self.assertIsNone(obj.z)
-
-        with self.assertRaises(ParserError):
-            self.parser.bind_dataclass({"x": 1, "y": "a"}, TypeD)
-
-    def test_bind_derived_dataclass(self):
-        data = {
-            "qname": "{urn:books}BookForm",
-            "type": None,
-            "value": {
-                "author": "Nagata, Suanne",
-                "title": "Becoming Somebody",
-            },
-        }
-
-        actual = self.parser.bind_dataclass(data, BookForm)
-        expected = DerivedElement(
-            qname="{urn:books}BookForm",
-            value=BookForm(author="Nagata, Suanne", title="Becoming Somebody"),
-            type=None,
-        )
-        self.assertEqual(expected, actual)
-
-    def test_bind_derived_dataclass_with_xsi_type(self):
-        data = {
-            "qname": "foobar",
-            "type": "{urn:books}BookForm",
-            "value": {
-                "author": "Nagata, Suanne",
-                "title": "Becoming Somebody",
-            },
-        }
-
-        actual = self.parser.bind_dataclass(data, DerivedElement)
-        expected = DerivedElement(
-            qname="foobar",
-            value=BookForm(author="Nagata, Suanne", title="Becoming Somebody"),
-            type="{urn:books}BookForm",
-        )
-        self.assertEqual(expected, actual)
-
-        with self.assertRaises(ParserError) as cm:
-            data["type"] = "notExists"
-            self.parser.bind_dataclass(data, DerivedElement)
-
-        self.assertEqual(
-            "Unable to locate derived model with" " properties(['author', 'title'])",
-            str(cm.exception),
-        )
-
-        with self.assertRaises(ParserError) as cm:
-            data["type"] = None
-            self.parser.bind_dataclass(data, DerivedElement)
-
-    def test_bind_dataclass_union(self):
-        data = {"element": {"x": 1, "y": "foo", "z": "1.0"}}
-        actual = self.parser.bind_dataclass(data, UnionType)
-
-        self.assertIsInstance(actual.element, TypeC)
-
-        with self.assertRaises(ParserError) as cm:
-            self.parser.bind_dataclass({"element": {"a": 1}}, UnionType)
-
-        self.assertEqual(
-            "Failed to bind object with properties(['a']) "
-            "to any of the ['TypeA', 'TypeB', 'TypeC', 'TypeD']",
-            str(cm.exception),
-        )
-
-    def test_bind_dataclass_subclasses(self):
-        data = {"element": {"x": "1", "y": "foo", "z": "1.0"}}
-        actual = self.parser.bind_dataclass(data, BaseType)
-
-        self.assertIsInstance(actual.element, BaseC)
-
-    def test_bind_attributes(self):
-        data = {"attrs": {"a": 1, "b": 2}, "index": 1}
-
-        actual = self.parser.bind_dataclass(data, AttrsType)
-        self.assertEqual(data["attrs"], actual.attrs)
-        self.assertIsNot(data["attrs"], actual.attrs)
-
-    def test_bind_simple_type_with_wildcard_var(self):
-        data = {"any": 1, "wildcard": 2}
-        actual = self.parser.bind_dataclass(data, ExtendedType)
-        self.assertEqual(1, actual.any)
-        self.assertEqual(2, actual.wildcard)
-
-    def test_bind_simple_type_with_elements_var(self):
-        data = {"choice": ["1.0", 1, ["1"], "a", "{a}b"]}
-
-        actual = self.parser.bind_dataclass(data, ChoiceType)
-
-        self.assertEqual(1.0, actual.choice[0])
-        self.assertEqual(1, actual.choice[1])
-        self.assertEqual([1], actual.choice[2])
-        self.assertEqual(QName("a"), actual.choice[3])
-        self.assertIsInstance(actual.choice[3], QName)
-        self.assertEqual(QName("{a}b"), actual.choice[4])
-        self.assertIsInstance(actual.choice[4], QName)
-
-        data = {"choice": ["!NotAQname"]}
-        with self.assertRaises(ParserError) as cm:
-            self.parser.bind_dataclass(data, ChoiceType)
-
-        self.assertEqual(
-            "Failed to bind '!NotAQname' to ChoiceType.choice field",
-            str(cm.exception),
-        )
-
-    def test_bind_simple_type_with_optional_elements_var(self):
-        data = {"a_or_b": None}
-        actual = self.parser.bind_dataclass(data, OptionalChoiceType)
-        self.assertIsNone(None, actual.a_or_b)
-
-    def test_bind_any_element(self):
-        data = {
-            "wildcard": {
-                "qname": "a",
-                "text": 1,
-                "tail": None,
-                "children": [],
-                "attributes": {},
-            }
-        }
-        self.assertEqual(
-            ExtendedType(wildcard=AnyElement(qname="a", text="1")),
-            self.parser.bind_dataclass(data, ExtendedType),
-        )
-
-    def test_bind_choice_dataclass(self):
-        data = {"choice": [{"x": 1}, {"x": 1, "y": "a"}]}
-        expected = ChoiceType(choice=[TypeA(x=1), TypeB(x=1, y="a")])
-        self.assertEqual(expected, self.parser.bind_dataclass(data, ChoiceType))
-
-    def test_bind_derived_value_with_simple_type(self):
-        data = {"choice": [{"qname": "int2", "value": 1, "type": None}]}
-
-        actual = self.parser.bind_dataclass(data, ChoiceType)
-        expected = ChoiceType(choice=[DerivedElement(qname="int2", value=1)])
-        self.assertEqual(expected, actual)
-
-    def test_bind_derived_value_with_choice_var(self):
-        data = {
-            "choice": [
-                {
-                    "qname": "b",
-                    "type": None,
-                    "value": {
-                        "x": "1",
-                        "y": "a",
-                    },
-                }
-            ]
-        }
-        expected = ChoiceType(
-            choice=[
-                DerivedElement(
-                    qname="b",
-                    value=TypeB(x=1, y="a"),
-                )
-            ]
-        )
-        self.assertEqual(expected, self.parser.bind_dataclass(data, ChoiceType))
-
-        with self.assertRaises(ParserError) as cm:
-            data["choice"][0]["qname"] = "nope"
-            self.parser.bind_dataclass(data, ChoiceType)
-
-        self.assertEqual(
-            "Unable to locate compound element ChoiceType.choice[nope]",
-            str(cm.exception),
-        )
-
-    def test_bind_wildcard_dataclass(self):
-        data = {"a": None, "wildcard": {"x": 1}}
-        expected = ExtendedType(wildcard=TypeA(x=1))
-        self.assertEqual(expected, self.parser.bind_dataclass(data, ExtendedType))
-
-    def test_bind_wildcard_with_derived_dataclass(self):
-        data = {
-            "wildcard": {
-                "qname": "b",
-                "type": "{xsdata}TypeB",
-                "value": {
-                    "x": "1",
-                    "y": "a",
-                },
-            }
-        }
-        expected = ExtendedType(
-            wildcard=DerivedElement(
-                qname="b", value=TypeB(x=1, y="a"), type="{xsdata}TypeB"
-            )
-        )
-        self.assertEqual(expected, self.parser.bind_dataclass(data, ExtendedType))
-
-    def test_bind_any_type_with_derived_dataclass(self):
-        data = {
-            "any": {
-                "qname": "any",
-                "type": None,
-                "value": {"x": "1"},
-            }
-        }
-        expected = ExtendedType(any=DerivedElement(qname="any", value=TypeA(x=1)))
-        self.assertEqual(expected, self.parser.bind_dataclass(data, ExtendedType))
-
-        with self.assertRaises(ParserError) as cm:
-            data["any"]["type"] = "notexists"
-            self.parser.bind_dataclass(data, ExtendedType)
-
-        self.assertEqual("Unable to locate xsi:type `notexists`", str(cm.exception))
-
-    def test_bind_text_with_unions(self):
-        Fixture = make_dataclass("Fixture", [("x", List[Union[int, float, str, bool]])])
-        values = ["foo", 12.2, "12.2", 12, "12", True, "false"]
-        data = json.dumps({"x": values})
-
-        result = self.parser.from_string(data, Fixture)
-        expected = ["foo", 12.2, 12.2, 12, 12, True, False]
-        self.assertEqual({"x": expected}, asdict(result))
-
-    def test_find_var(self):
-        meta = self.parser.context.build(TypeB)
-        xml_vars = meta.get_all_vars()
-
-        self.assertEqual(xml_vars[0], self.parser.find_var(xml_vars, "x"))
-        self.assertEqual(xml_vars[0], self.parser.find_var(xml_vars, "x", True))
-
-        meta = self.parser.context.build(ExtendedType)
-        xml_vars = meta.get_all_vars()
-        self.assertIsNone(self.parser.find_var(xml_vars, "a", True))
-        self.assertEqual(xml_vars[0], self.parser.find_var(xml_vars, "a"))
diff --git a/tests/formats/dataclass/serializers/test_dict.py b/tests/formats/dataclass/serializers/test_dict.py
new file mode 100644
index 00000000..cd7b683b
--- /dev/null
+++ b/tests/formats/dataclass/serializers/test_dict.py
@@ -0,0 +1,139 @@
+import json
+from unittest.case import TestCase
+
+from tests.fixtures.books import BookForm, Books
+from tests.fixtures.datatypes import Telephone
+from xsdata.exceptions import XmlContextError
+from xsdata.formats.dataclass.serializers import DictEncoder
+from xsdata.formats.dataclass.serializers.json import DictFactory, JsonSerializer
+from xsdata.models.datatype import XmlDate
+from xsdata.models.xsd import Attribute
+from xsdata.utils.testing import XmlVarFactory
+
+
+class DictEncoderTests(TestCase):
+    def setUp(self):
+        super().setUp()
+        self.encoder = DictEncoder(dict_factory=DictFactory.FILTER_NONE)
+        self.books = Books(
+            book=[
+                BookForm(
+                    id="bk001",
+                    author="Hightower, Kim",
+                    title="The First Book",
+                    genre="Fiction",
+                    price=44.95,
+                    pub_date=XmlDate.from_string("2000-10-01"),
+                    review="An amazing story of nothing.",
+                ),
+                BookForm(
+                    id="bk002",
+                    author="Nagata, Suanne",
+                    title="Becoming Somebody",
+                    genre="Biography",
+                    review="A masterpiece of the fine art of gossiping.",
+                ),
+            ]
+        )
+
+        self.expected = {
+            "book": [
+                {
+                    "author": "Hightower, Kim",
+                    "genre": "Fiction",
+                    "id": "bk001",
+                    "lang": "en",
+                    "price": 44.95,
+                    "pub_date": "2000-10-01",
+                    "review": "An amazing story of nothing.",
+                    "title": "The First Book",
+                },
+                {
+                    "author": "Nagata, Suanne",
+                    "genre": "Biography",
+                    "id": "bk002",
+                    "lang": "en",
+                    "review": "A masterpiece of the fine art of gossiping.",
+                    "title": "Becoming Somebody",
+                },
+            ]
+        }
+
+    def test_encode(self):
+        actual = self.encoder.encode(self.books)
+        self.assertEqual(self.expected, actual)
+
+    def test_encode_a_none_dataclass_object(self):
+        with self.assertRaises(XmlContextError):
+            DictEncoder().encode(1)
+
+    def test_encode_list_of_objects(self):
+        actual = self.encoder.encode(self.books.book)
+        self.assertEqual(self.expected["book"], actual)
+
+    def test_encode_with_enum(self):
+        obj = Attribute()
+        serializer = JsonSerializer(dict_factory=DictFactory.FILTER_NONE)
+        actual = json.loads(serializer.render(obj))
+
+        self.assertEqual("optional", actual["use"])
+
+    def test_convert_namedtuple(self):
+        var = XmlVarFactory.create(types=(Telephone,))
+        actual = self.encoder.encode(Telephone(30, 234, 56783), var)
+        self.assertEqual("30-234-56783", actual)
+
+    def test_next_value(self):
+        book = self.books.book[0]
+        serializer = JsonSerializer()
+
+        actual = [name for name, value in serializer.next_value(book)]
+        expected = [
+            "author",
+            "title",
+            "genre",
+            "price",
+            "pub_date",
+            "review",
+            "id",
+            "lang",
+        ]
+        self.assertEqual(expected, actual)
+
+        serializer.config.ignore_default_attributes = True
+        expected = expected[:-1]
+        actual = [name for name, value in serializer.next_value(book)]
+        self.assertEqual(expected, actual)
+
+    def test_pretty_print_indent(self):
+        serializer = JsonSerializer()
+        serializer.config.pretty_print = True
+        serializer.config.pretty_print_indent = "    "
+
+        actual = serializer.render(self.books)
+        expected = f"""{{
+    "book": [
+        {{
+            "author": "{self.expected["book"][0]["author"]}",
+            "title": "{self.expected["book"][0]["title"]}",
+            "genre": "{self.expected["book"][0]["genre"]}",
+            "price": {self.expected["book"][0]["price"]},
+            "pub_date": "{self.expected["book"][0]["pub_date"]}",
+            "review": "{self.expected["book"][0]["review"]}",
+            "id": "{self.expected["book"][0]["id"]}",
+            "lang": "en"
+        }},
+        {{
+            "author": "{self.expected["book"][1]["author"]}",
+            "title": "{self.expected["book"][1]["title"]}",
+            "genre": "{self.expected["book"][1]["genre"]}",
+            "price": null,
+            "pub_date": null,
+            "review": "{self.expected["book"][1]["review"]}",
+            "id": "{self.expected["book"][1]["id"]}",
+            "lang": "en"
+        }}
+    ]
+}}"""
+
+        self.assertEqual(expected, actual)
diff --git a/tests/formats/dataclass/serializers/test_json.py b/tests/formats/dataclass/serializers/test_json.py
index 34e14347..66ddfdf4 100644
--- a/tests/formats/dataclass/serializers/test_json.py
+++ b/tests/formats/dataclass/serializers/test_json.py
@@ -2,12 +2,8 @@ import json
 from unittest.case import TestCase
 
 from tests.fixtures.books import BookForm, Books
-from tests.fixtures.datatypes import Telephone
-from xsdata.exceptions import XmlContextError
 from xsdata.formats.dataclass.serializers.json import DictFactory, JsonSerializer
 from xsdata.models.datatype import XmlDate
-from xsdata.models.xsd import Attribute
-from xsdata.utils.testing import XmlVarFactory
 
 
 class JsonSerializerTests(TestCase):
@@ -63,50 +59,6 @@ class JsonSerializerTests(TestCase):
 
         self.assertEqual(self.expected, json.loads(actual))
 
-    def test_render_a_none_dataclass_object(self):
-        with self.assertRaises(XmlContextError):
-            JsonSerializer().render(1)
-
-    def test_render_list_of_objects(self):
-        serializer = JsonSerializer(dict_factory=DictFactory.FILTER_NONE)
-        actual = serializer.render(self.books.book)
-        self.assertEqual(self.expected["book"], json.loads(actual))
-
-    def test_render_with_enum(self):
-        obj = Attribute()
-        serializer = JsonSerializer(dict_factory=DictFactory.FILTER_NONE)
-        actual = json.loads(serializer.render(obj))
-
-        self.assertEqual("optional", actual["use"])
-
-    def test_convert_namedtuple(self):
-        var = XmlVarFactory.create(types=(Telephone,))
-        serializer = JsonSerializer(dict_factory=DictFactory.FILTER_NONE)
-        actual = serializer.convert(Telephone(30, 234, 56783), var)
-        self.assertEqual("30-234-56783", actual)
-
-    def test_next_value(self):
-        book = self.books.book[0]
-        serializer = JsonSerializer()
-
-        actual = [name for name, value in serializer.next_value(book)]
-        expected = [
-            "author",
-            "title",
-            "genre",
-            "price",
-            "pub_date",
-            "review",
-            "id",
-            "lang",
-        ]
-        self.assertEqual(expected, actual)
-
-        serializer.config.ignore_default_attributes = True
-        expected = expected[:-1]
-        actual = [name for name, value in serializer.next_value(book)]
-        self.assertEqual(expected, actual)
-
     def test_pretty_print_indent(self):
         serializer = JsonSerializer()
         serializer.config.pretty_print = True
diff --git a/tests/formats/dataclass/test_client.py b/tests/formats/dataclass/test_client.py
index 80771d4a..a68a1198 100644
--- a/tests/formats/dataclass/test_client.py
+++ b/tests/formats/dataclass/test_client.py
@@ -9,6 +9,8 @@ from tests.fixtures.calculator import (
 from tests.fixtures.hello import HelloGetHelloAsString
 from xsdata.exceptions import ClientValueError
 from xsdata.formats.dataclass.client import Client, Config, TransportTypes
+from xsdata.formats.dataclass.parsers import XmlParser
+from xsdata.formats.dataclass.serializers import XmlSerializer
 from xsdata.formats.dataclass.transports import DefaultTransport
 
 response = """
@@ -26,6 +28,18 @@ response = """
 
 
 class ClientTests(TestCase):
+    def test__init__(self):
+        config = Config.from_service(CalculatorSoapAdd, transport="foobar")
+
+        client = Client(config)
+        self.assertIs(client.parser.context, client.serializer.context)
+
+        client = Client(config, parser=XmlParser())
+        self.assertIs(client.parser.context, client.serializer.context)
+
+        client = Client(config, serializer=XmlSerializer())
+        self.assertIs(client.parser.context, client.serializer.context)
+
     def test_from_service(self):
         client = Client.from_service(CalculatorSoapAdd, location="http://testurl.com")