Repository: Fatal1ty/mashumaro (language: python)

Issue:
Add from_* functions to parse without inheritance for nested structures
**Is your feature request related to a problem? Please describe.**
I'd like to switch from dacite to this, but that would entail modifying every class on a structure to inherit from a specific mixin.

**Describe the solution you'd like**
It'd be great to have functions in the family of `from_*` that could be called on plain dataclasses to generate a compiled parsing function.

```python
from mashumaro import from_yaml
from dataclasses import dataclass

@dataclass
class MySection:
  field: str

@dataclass
class MyConfig:
  section: MySection

# Compilation happens here, the returned callable parses the contents
config_from_yaml = from_yaml(MyConfig)
with open("./config.yaml") as file:
  config_from_yaml(file.read())
```

**Describe alternatives you've considered**
I've tried a more naive approach but it does not work for nested structures.

```python
from mashumaro.mixins.yaml import DataClassYAMLMixin
from typing import TypeVar, Type, Callable
T = TypeVar("T")
def from_yaml(type_: Type[T]) -> Callable[[str], T]:
  @dataclass
  class Inheritancent(type_, DataClassYAMLMixin):
    pass
  return Inheritancent.from_yaml
```

```text
>>> from mashumaro.mixins.yaml import DataClassYAMLMixin
>>> T = TypeVar("T")
>>> def from_yaml(type_: Type[T]) -> Callable[[str], T]:
...   @dataclass
...   class Inheritancent(type_, DataClassYAMLMixin):
...     pass
...   def parse_from_yaml(contents: str):
...     return Inheritancent.from_yaml(contents)
... 
>>> from dataclasses import dataclass
>>> 
>>> @dataclass
... class MySection:
...   field: str
... 
>>> @dataclass
... class MyConfig:
...   section: MySection
... 
>>> from_yaml(MyConfig)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in from_yaml
  File "/home/fakui/.local/lib/python3.10/site-packages/mashumaro/mixins/dict.py", line 24, in __init_subclass__
    compile_mixin_unpacker(cls, **builder_params["unpacker"])
  File "/home/fakui/.local/lib/python3.10/site-packages/mashumaro/core/meta/mixin.py", line 49, in compile_mixin_unpacker
    builder.add_unpack_method()
  File "/home/fakui/.local/lib/python3.10/site-packages/mashumaro/core/meta/code/builder.py", line 398, in add_unpack_method
    self._add_unpack_method_lines(method_name)
  File "/home/fakui/.local/lib/python3.10/site-packages/mashumaro/core/meta/code/builder.py", line 313, in _add_unpack_method_lines
    self._unpack_method_set_value(
  File "/home/fakui/.local/lib/python3.10/site-packages/mashumaro/core/meta/code/builder.py", line 440, in _unpack_method_set_value
    unpacked_value = UnpackerRegistry.get(
  File "/home/fakui/.local/lib/python3.10/site-packages/mashumaro/core/meta/types/common.py", line 95, in get
    raise UnserializableField(
mashumaro.exceptions.UnserializableField: Field "section" of type MySection in from_yaml.<locals>.Inheritancent is not serializable
```



Discussion / hints:
This is something I've been asked for many times. I do have plans to do this, but I haven't yet figured out what a practical and customizable method builder should look like. Maybe you have some ideas.

Related issue:
* https://github.com/Fatal1ty/mashumaro/issues/69
I'm not at all familiar with all the customizations provided by this module, however if I had to hack in support for this I'd start by making a recursive function to swap out all the dataclasses in a dataclass by serializable ones using the method I proposed in the issue post. I would personally not bother much with all the customization options for now since if your use case is very specific you'd probably be better off by doing this the "proper" way and defining the structures yourself.
> I've tried a more naive approach but it does not work for nested structures.
> 
> ```python
> from mashumaro.mixins.yaml import DataClassYAMLMixin
> from typing import TypeVar, Type, Callable
> T = TypeVar("T")
> def from_yaml(type_: Type[T]) -> Callable[[str], T]:
>   @dataclass
>   class Inheritancent(type_, DataClassYAMLMixin):
>     pass
>   return Inheritancent.from_yaml
> ```

@fakuivan It will work now with https://github.com/Fatal1ty/mashumaro/pull/131 merged

A more practical way could be as follows:
```python
from dataclasses import dataclass
from typing import Callable, Type, TypeVar

from mashumaro.core.meta.code.builder import CodeBuilder
from mashumaro.mixins.yaml import default_decoder, default_encoder

T = TypeVar("T")


def from_yaml(type_: Type[T]) -> Callable[[str], T]:
    cb = CodeBuilder(type_, format_name="yaml", decoder=default_decoder)
    cb.add_unpack_method()
    return getattr(type_, "__mashumaro_from_yaml__")


@dataclass
class MySection:
    field: str


@dataclass
class MyConfig:
    section: MySection


data = """
section:
  field: foobar
"""


print(from_yaml(MyConfig)(data)). # MyConfig(section=MySection(field='foobar'))
```

I know it's still a workaround and there's still a few kinks to work out, but it's a start.

Good news everyone! I’m working on it, so this functionality will be a part of 3.11 release.

Fix patch:
diff --git a/.coveragerc b/.coveragerc
deleted file mode 100644
index 6e909722..00000000
--- a/.coveragerc
+++ /dev/null
@@ -1,2 +0,0 @@
-[run]
-omit = setup.py
diff --git a/README.md b/README.md
index 26eeda5e..0e30a1da 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 <img alt="logo" width="175" src="https://raw.githubusercontent.com/Fatal1ty/mashumaro/ac2f924591d488dbd9a776a6b1ae7dede2d8c73e/img/logo.svg">
 
-###### Fast and well tested serialization library on top of dataclasses
+###### Fast and well tested serialization library
 
 [![Build Status](https://github.com/Fatal1ty/mashumaro/workflows/tests/badge.svg)](https://github.com/Fatal1ty/mashumaro/actions)
 [![Coverage Status](https://coveralls.io/repos/github/Fatal1ty/mashumaro/badge.svg?branch=master)](https://coveralls.io/github/Fatal1ty/mashumaro?branch=master)
@@ -11,35 +11,35 @@
 [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
 </div>
 
-When using dataclasses, you often need to dump and load objects based on the
-schema you have. Mashumaro not only lets you save and load things in different
-ways, but it also does it _super quick_.
+In Python, you often need to dump and load objects based on the schema you
+have. It can be a dataclass model, a list of third-party generic classes or
+whatever. Mashumaro not only lets you save and load things in different ways,
+but it also does it _super quick_.
 
 **Key features**
 * 🚀 One of the fastest libraries
 * ☝️ Mature and time-tested
 * 👶 Easy to use out of the box
 * ⚙️ Highly customizable
-* 🎉 Built-in support for JSON, YAML, MessagePack, TOML
+* 🎉 Built-in support for JSON, YAML, TOML, MessagePack
 * 📦 Built-in support for almost all Python types including typing-extensions
 * 📝 JSON Schema generation
 
 Table of contents
---------------------------------------------------------------------------------
+-------------------------------------------------------------------------------
+* [Introduction](#introduction)
 * [Installation](#installation)
 * [Changelog](#changelog)
-* [Supported serialization formats](#supported-serialization-formats)
 * [Supported data types](#supported-data-types)
 * [Usage example](#usage-example)
 * [How does it work?](#how-does-it-work)
 * [Benchmark](#benchmark)
-* [Serialization mixins](#serialization-mixins)
-  * [`DataClassDictMixin`](#dataclassdictmixin)
-  * [`DataClassJSONMixin`](#dataclassjsonmixin)
-  * [`DataClassORJSONMixin`](#dataclassorjsonmixin)
-  * [`DataClassMessagePackMixin`](#dataclassmessagepackmixin)
-  * [`DataClassYAMLMixin`](#dataclassyamlmixin)
-  * [`DataClassTOMLMixin`](#dataclasstomlmixin)
+* [Supported serialization formats](#supported-serialization-formats)
+  * [Basic form](#basic-form)
+  * [JSON](#json)
+  * [YAML](#yaml)
+  * [TOML](#toml)
+  * [MessagePack](#messagepack)
 * [Customization](#customization)
     * [`SerializableType` interface](#serializabletype-interface)
       * [User-defined types](#user-defined-types)
@@ -72,8 +72,10 @@ Table of contents
     * [Extending existing types](#extending-existing-types)
     * [Dialects](#dialects)
       * [`serialization_strategy` dialect option](#serialization_strategy-dialect-option)
+      * [`serialize_by_alias` dialect option](#serialize_by_alias-dialect-option)
       * [`omit_none` dialect option](#omit_none-dialect-option)
       * [`omit_default` dialect option](#omit_default-dialect-option)
+      * [`named_tuple_as_dict` dialect option](#namedtuple_as_dict-dialect-option)
       * [`no_copy_collections` dialect option](#no_copy_collections-dialect-option)
       * [Changing the default dialect](#changing-the-default-dialect)
     * [Discriminator](#discriminator)
@@ -102,8 +104,34 @@ Table of contents
     * [Extending JSON Schema](#extending-json-schema)
     * [JSON Schema and custom serialization methods](#json-schema-and-custom-serialization-methods)
 
+Introduction
+-------------------------------------------------------------------------------
+
+This library provides two fundamentally different approaches to converting
+your data to and from various formats. Each of them is useful in different
+situations:
+
+* Codecs
+* Mixins
+
+Codecs are represented by a set of decoder / encoder classes and
+decode / encode functions for each supported format. You can use them
+to convert data of any python built-in and third-party type to JSON, YAML,
+TOML, MessagePack or a basic form accepted by other serialization formats.
+For example, you can convert a list of datetime objects to JSON array
+containing string-represented datetimes and vice versa.
+
+Mixins are primarily for dataclass models. They are represented by mixin
+classes that add methods for converting to and from JSON, YAML, TOML,
+MessagePack or a basic form accepted by other serialization formats.
+If you have a root dataclass model, then it will be the easiest way to make it
+serializable. All you have to do is inherit a particular mixin class.
+
+In addition to serialization functionality, this library also provides JSON
+Schema builder that can be used in places where interoperability matters.
+
 Installation
---------------------------------------------------------------------------------
+-------------------------------------------------------------------------------
 
 Use pip to install:
 ```shell
@@ -127,30 +155,13 @@ of Python.
 
 
 Changelog
---------------------------------------------------------------------------------
+-------------------------------------------------------------------------------
 
 This project follows the principles of [Semantic Versioning](https://semver.org).
 Changelog is available on [GitHub Releases page](https://github.com/Fatal1ty/mashumaro/releases).
 
-Supported serialization formats
---------------------------------------------------------------------------------
-
-This library adds methods for dumping to and loading from the
-following formats:
-
-* [plain dict](https://docs.python.org/3/library/stdtypes.html#mapping-types-dict)
-* [JSON](https://www.json.org)
-* [YAML](https://yaml.org)
-* [MessagePack](https://msgpack.org)
-* [TOML](https://toml.io)
-
-Plain dict can be useful when you need to pass a dict object to a
-third-party library, such as a client for MongoDB.
-
-You can find the documentation for the specific serialization format [below](#serialization-mixins).
-
 Supported data types
---------------------------------------------------------------------------------
+-------------------------------------------------------------------------------
 
 There is support for generic types from the standard [`typing`](https://docs.python.org/3/library/typing.html) module:
 * [`List`](https://docs.python.org/3/library/typing.html#typing.List)
@@ -266,35 +277,49 @@ for arbitrary types:
 * [third-party generic types](#third-party-generic-types)
 
 Usage example
---------------------------------------------------------------------------------
+-------------------------------------------------------------------------------
+
+Suppose we're developing a financial application and we operate with currencies
+and stocks:
 
 ```python
-from enum import Enum
-from typing import List
 from dataclasses import dataclass
-from mashumaro.mixins.json import DataClassJSONMixin
+from enum import Enum
 
 class Currency(Enum):
     USD = "USD"
     EUR = "EUR"
 
 @dataclass
-class CurrencyPosition(DataClassJSONMixin):
+class CurrencyPosition:
     currency: Currency
     balance: float
 
 @dataclass
-class StockPosition(DataClassJSONMixin):
+class StockPosition:
     ticker: str
     name: str
     balance: int
+```
+
+Now we want a dataclass for portfolio that will be serialized to and from JSON.
+We inherit `DataClassJSONMixin` that adds this functionality:
+
+```python
+from mashumaro.mixins.json import DataClassJSONMixin
+
+...
 
 @dataclass
 class Portfolio(DataClassJSONMixin):
-    currencies: List[CurrencyPosition]
-    stocks: List[StockPosition]
+    currencies: list[CurrencyPosition]
+    stocks: list[StockPosition]
+```
+
+Let's create a portfolio instance and check methods `from_json` and `to_json`:
 
-my_portfolio = Portfolio(
+```python
+portfolio = Portfolio(
     currencies=[
         CurrencyPosition(Currency.USD, 238.67),
         CurrencyPosition(Currency.EUR, 361.84),
@@ -305,26 +330,48 @@ my_portfolio = Portfolio(
     ]
 )
 
-json_string = my_portfolio.to_json()
-Portfolio.from_json(json_string)  # same as my_portfolio
+portfolio_json = portfolio.to_json()
+assert Portfolio.from_json(portfolio_json) == portfolio
+```
+
+If we need to serialize something different from a root dataclass,
+we can use codecs. In the following example we create a JSON decoder and encoder
+for a list of currencies:
+
+```python
+from mashumaro.codecs.json import JSONDecoder, JSONEncoder
+
+...
+
+decoder = JSONDecoder(list[CurrencyPosition])
+encoder = JSONEncoder(list[CurrencyPosition])
+
+currencies = [
+    CurrencyPosition(Currency.USD, 238.67),
+    CurrencyPosition(Currency.EUR, 361.84),
+]
+currencies_json = encoder.encode(currencies)
+assert decoder.decode(currencies_json) == currencies
+
 ```
 
 How does it work?
---------------------------------------------------------------------------------
+-------------------------------------------------------------------------------
 
 This library works by taking the schema of the data and generating a
-specific parser and builder for exactly that schema, taking into account the
-specifics of the serialization format. This is much faster than inspection of
-field types on every call of parsing or building at runtime.
-
-These specific parsers and builders are presented by the corresponding
-`from_*` and `to_*` methods. They are compiled during import time (or at
-runtime in some cases) and are set as attributes to your dataclasses.
-To minimize the import time, you can explicitly enable
+specific decoder and encoder for exactly that schema, taking into account the
+specifics of serialization format. This is much faster than inspection of
+data types on every call of decoding or encoding at runtime.
+
+These specific decoders and encoders are generated by
+[codecs and mixins](#supported-serialization-formats). When using serialization
+mixins, these methods are compiled during import time (or at runtime in some
+cases) and are set as attributes to your dataclasses. To minimize the import
+time, you can explicitly enable
 [lazy compilation](#lazy_compilation-config-option).
 
 Benchmark
---------------------------------------------------------------------------------
+-------------------------------------------------------------------------------
 
 * macOS 13.3.1 Ventura
 * Apple M1
@@ -362,136 +409,356 @@ pip install -r requirements-dev.txt
 ./benchmark/run.sh
 ```
 
-Serialization mixins
---------------------------------------------------------------------------------
+Supported serialization formats
+-------------------------------------------------------------------------------
 
-`mashumaro` provides mixins for each serialization format.
+This library has built-in support for multiple popular formats:
 
-#### [`DataClassDictMixin`](https://github.com/Fatal1ty/mashumaro/blob/master/mashumaro/mixins/dict.py#L14)
+* [JSON](https://www.json.org)
+* [YAML](https://yaml.org)
+* [TOML](https://toml.io)
+* [MessagePack](https://msgpack.org)
+
+There are preconfigured codecs and mixin classes. However, you're free
+to override some settings if necessary.
+
+> [!IMPORTANT]\
+> As for codecs, you are
+> offered to choose between convenience and efficiency. When you need to decode
+> or encode structured data more than once, it's highly recommended to create
+> and reuse a decoder or encoder specifically for that structure. For one-time
+> use with default settings it may be convenient to use global functions that
+> create a disposable decoder or encoder under the hood. Be aware not to use
+> these convenient global functions multiple times for the same structure.
+
+### Basic form
+
+Basic form denotes a python object consisting only of basic data types
+supported by most serialization formats. These types are:
+[`str`](https://docs.python.org/3/library/stdtypes.html#str),
+[`int`](https://docs.python.org/3/library/functions.html#int),
+[`float`](https://docs.python.org/3/library/functions.html#float),
+[`bool`](https://docs.python.org/3/library/stdtypes.html#bltin-boolean-values),
+[`list`](https://docs.python.org/3/library/stdtypes.html#list),
+[`dict`](https://docs.python.org/3/library/stdtypes.html#dict).
+
+This is also a starting point you can play with for a comprehensive
+transformation of your data.
+
+Efficient decoder and encoder can be used as follows:
 
-Can be imported in two ways:
+```python
+from mashumaro.codecs import BasicDecoder, BasicEncoder
+# or from mashumaro.codecs.basic import BasicDecoder, BasicEncoder
+
+decoder = BasicDecoder(<shape_type>, ...)
+decoder.decode(...)
+
+encoder = BasicEncoder(<shape_type>, ...)
+encoder.encode(...)
+```
+
+Convenient functions are recommended to be used as follows:
+```python
+import mashumaro.codecs.basic as basic_codec
+
+basic_codec.decode(..., <shape_type>)
+basic_codec.encode(..., <shape_type>)
+```
+
+Mixin can be used as follows:
 ```python
 from mashumaro import DataClassDictMixin
-from mashumaro.mixins.dict import DataClassDictMixin
+# or from mashumaro.mixins.dict import DataClassDictMixin
+
+@dataclass
+class MyModel(DataClassDictMixin):
+    ...
+
+MyModel.from_dict(...)
+MyModel(...).to_dict()
 ```
 
-The core mixin that adds serialization functionality to a dataclass.
-This mixin is a base class for all other serialization format mixins.
-It adds methods [`from_dict`](https://github.com/Fatal1ty/mashumaro/blob/master/mashumaro/mixins/dict.py#L38-L47)
-and [`to_dict`](https://github.com/Fatal1ty/mashumaro/blob/master/mashumaro/mixins/dict.py#L27-L36).
+> [!TIP]\
+> You don't need to inherit `DataClassDictMixin` along with other serialization
+> mixins because it's a base class for them.
+
+### JSON
 
-#### [`DataClassJSONMixin`](https://github.com/Fatal1ty/mashumaro/blob/master/mashumaro/mixins/json.py#L14)
+[JSON](https://www.json.org) is a lightweight data-interchange format. You can
+choose between standard library
+[json](https://docs.python.org/3/library/json.html) for compatibility and
+third-party dependency [orjson](https://pypi.org/project/orjson/) for better
+performance.
 
-Can be imported as:
+#### json library
+
+Efficient decoder and encoder can be used as follows:
 ```python
-from mashumaro.mixins.json import DataClassJSONMixin
+from mashumaro.codecs.json import JSONDecoder, JSONEncoder
+
+decoder = JSONDecoder(<shape_type>, ...)
+decoder.decode(...)
+
+encoder = JSONEncoder(<shape_type>, ...)
+encoder.encode(...)
 ```
 
-This mixin adds json serialization functionality to a dataclass.
-It adds methods [`from_json`](https://github.com/Fatal1ty/mashumaro/blob/master/mashumaro/mixins/json.py#L24-L31)
-and [`to_json`](https://github.com/Fatal1ty/mashumaro/blob/master/mashumaro/mixins/json.py#L17-L22).
+Convenient functions can be used as follows:
+```python
+from mashumaro.codecs.json import json_decode, json_encode
 
-#### [`DataClassORJSONMixin`](https://github.com/Fatal1ty/mashumaro/blob/master/mashumaro/mixins/orjson.py#L30)
+json_decode(..., <shape_type>)
+json_encode(..., <shape_type>)
+```
 
-Can be imported as:
+Convenient function aliases are recommended to be used as follows:
 ```python
-from mashumaro.mixins.orjson import DataClassORJSONMixin
+import mashumaro.codecs.json as json_codec
+
+json_codec.decode(...<shape_type>)
+json_codec.encode(..., <shape_type>)
 ```
 
-This mixin adds json serialization functionality to a dataclass using
-a third-party [`orjson`](https://pypi.org/project/orjson/) library.
-It adds methods [`from_json`](https://github.com/Fatal1ty/mashumaro/blob/master/mashumaro/mixins/orjson.pyi#L33-L39),
-[`to_jsonb`](https://github.com/Fatal1ty/mashumaro/blob/master/mashumaro/mixins/orjson.pyi#L19-L25),
-[`to_json`](https://github.com/Fatal1ty/mashumaro/blob/master/mashumaro/mixins/orjson.pyi#L26-L32).
+Mixin can be used as follows:
+```python
+from mashumaro.mixins.json import DataClassJSONMixin
 
-In order to use this mixin, the [`orjson`](https://pypi.org/project/orjson/) package must be installed.
-You can install it manually or using an extra option for `mashumaro`:
+@dataclass
+class MyModel(DataClassJSONMixin):
+    ...
+
+MyModel.from_json(...)
+MyModel(...).to_json()
+```
+
+#### orjson library
+
+In order to use [`orjson`](https://pypi.org/project/orjson/) library, it must
+be installed manually or using an extra option for `mashumaro`:
 
 ```shell
 pip install mashumaro[orjson]
 ```
 
-Using this mixin the following data types will be handled by
+The following data types will be handled by
 [`orjson`](https://pypi.org/project/orjson/) library by default:
 * [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime)
 * [`date`](https://docs.python.org/3/library/datetime.html#datetime.date)
 * [`time`](https://docs.python.org/3/library/datetime.html#datetime.time)
 * [`uuid.UUID`](https://docs.python.org/3/library/uuid.html#uuid.UUID)
 
-#### [`DataClassMessagePackMixin`](https://github.com/Fatal1ty/mashumaro/blob/master/mashumaro/mixins/msgpack.py#L36)
-
-Can be imported as:
+Efficient decoder and encoder can be used as follows:
 ```python
-from mashumaro.mixins.msgpack import DataClassMessagePackMixin
-```
+from mashumaro.codecs.orjson import ORJSONDecoder, ORJSONEncoder
 
-This mixin adds MessagePack serialization functionality to a dataclass.
-It adds methods [`from_msgpack`](https://github.com/Fatal1ty/mashumaro/blob/master/mashumaro/mixins/msgpack.py#L59-L66)
-and [`to_msgpack`](https://github.com/Fatal1ty/mashumaro/blob/master/mashumaro/mixins/msgpack.py#L52-L57).
+decoder = ORJSONDecoder(<shape_type>, ...)
+decoder.decode(...)
 
-In order to use this mixin, the [`msgpack`](https://pypi.org/project/msgpack/) package must be installed.
-You can install it manually or using an extra option for `mashumaro`:
+encoder = ORJSONEncoder(<shape_type>, ...)
+encoder.encode(...)
+```
 
-```shell
-pip install mashumaro[msgpack]
+Convenient functions can be used as follows:
+```python
+from mashumaro.codecs.orjson import json_decode, json_encode
+
+json_decode(..., <shape_type>)
+json_encode(..., <shape_type>)
 ```
 
-Using this mixin the following data types will be handled by
-[`msgpack`](https://pypi.org/project/msgpack/) library by default:
-* [`bytes`](https://docs.python.org/3/library/stdtypes.html#bytes)
-* [`bytearray`](https://docs.python.org/3/library/stdtypes.html#bytearray)
+Convenient function aliases are recommended to be used as follows:
+```python
+import mashumaro.codecs.orjson as json_codec
 
-#### [`DataClassYAMLMixin`](https://github.com/Fatal1ty/mashumaro/blob/master/mashumaro/mixins/yaml.py#L27)
+json_codec.decode(...<shape_type>)
+json_codec.encode(..., <shape_type>)
+```
 
-Can be imported as:
+Mixin can be used as follows:
 ```python
-from mashumaro.mixins.yaml import DataClassYAMLMixin
+from mashumaro.mixins.orjson import DataClassORJSONMixin
+
+@dataclass
+class MyModel(DataClassORJSONMixin):
+    ...
+
+MyModel.from_json(...)
+MyModel(...).to_json()
+MyModel(...).to_jsonb()
 ```
 
-This mixin adds YAML serialization functionality to a dataclass.
-It adds methods [`from_yaml`](https://github.com/Fatal1ty/mashumaro/blob/master/mashumaro/mixins/yaml.py#L37-L44)
-and [`to_yaml`](https://github.com/Fatal1ty/mashumaro/blob/master/mashumaro/mixins/yaml.py#L30-L35).
+### YAML
 
-In order to use this mixin, the [`pyyaml`](https://pypi.org/project/PyYAML/) package must be installed.
+[YAML](https://yaml.org) is a human-friendly data serialization language for
+all programming languages. In order to use this format, the
+[`pyyaml`](https://pypi.org/project/PyYAML/) package must be installed.
 You can install it manually or using an extra option for `mashumaro`:
 
 ```shell
 pip install mashumaro[yaml]
 ```
 
-#### [`DataClassTOMLMixin`](https://github.com/Fatal1ty/mashumaro/blob/master/mashumaro/mixins/toml.py#L33)
+Efficient decoder and encoder can be used as follows:
+```python
+from mashumaro.codecs.yaml import YAMLDecoder, YAMLEncoder
+
+decoder = YAMLDecoder(<shape_type>, ...)
+decoder.decode(...)
 
-Can be imported as:
+encoder = YAMLEncoder(<shape_type>, ...)
+encoder.encode(...)
+```
+
+Convenient functions can be used as follows:
 ```python
-from mashumaro.mixins.toml import DataClassTOMLMixin
+from mashumaro.codecs.yaml import yaml_decode, yaml_encode
+
+yaml_decode(..., <shape_type>)
+yaml_encode(..., <shape_type>)
+```
+
+Convenient function aliases are recommended to be used as follows:
+```python
+import mashumaro.codecs.yaml as yaml_codec
+
+yaml_codec.decode(...<shape_type>)
+yaml_codec.encode(..., <shape_type>)
+```
+
+Mixin can be used as follows:
+```python
+from mashumaro.mixins.yaml import DataClassYAMLMixin
+
+@dataclass
+class MyModel(DataClassYAMLMixin):
+    ...
+
+MyModel.from_yaml(...)
+MyModel(...).to_yaml()
 ```
 
-This mixin adds TOML serialization functionality to a dataclass.
-It adds methods [`from_toml`](https://github.com/Fatal1ty/mashumaro/blob/master/mashumaro/mixins/toml.py#L56-L63)
-and [`to_toml`](https://github.com/Fatal1ty/mashumaro/blob/master/mashumaro/mixins/toml.py#L49-L54).
+### TOML
 
-In order to use this mixin, the [`tomli`](https://pypi.org/project/tomli/) and
+[TOML](https://toml.io) is config file format for humans.
+In order to use this format, the [`tomli`](https://pypi.org/project/tomli/) and
 [`tomli-w`](https://pypi.org/project/tomli-w/) packages must be installed.
 In Python 3.11+, `tomli` is included as
 [`tomlib`](https://docs.python.org/3/library/tomllib.html) standard library
-module and can be used my this mixin.
-You can install the missing packages manually or using an extra option for `mashumaro`:
+module and is used for this format. You can install the missing packages
+manually or using an extra option
+for `mashumaro`:
 
 ```shell
 pip install mashumaro[toml]
 ```
 
-Using this mixin the following data types will be handled by
+The following data types will be handled by
 [`tomli`](https://pypi.org/project/tomli/)/
 [`tomli-w`](https://pypi.org/project/tomli-w/) library by default:
 * [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime)
 * [`date`](https://docs.python.org/3/library/datetime.html#datetime.date)
 * [`time`](https://docs.python.org/3/library/datetime.html#datetime.time)
 
-Fields with value `None` will be omitted on serialization because TOML doesn't support null values.
+Fields with value `None` will be omitted on serialization because TOML
+doesn't support null values.
+
+Efficient decoder and encoder can be used as follows:
+```python
+from mashumaro.codecs.toml import TOMLDecoder, TOMLEncoder
+
+decoder = TOMLDecoder(<shape_type>, ...)
+decoder.decode(...)
+
+encoder = TOMLEncoder(<shape_type>, ...)
+encoder.encode(...)
+```
+
+Convenient functions can be used as follows:
+```python
+from mashumaro.codecs.toml import toml_decode, toml_encode
+
+toml_decode(..., <shape_type>)
+toml_encode(..., <shape_type>)
+```
+
+Convenient function aliases are recommended to be used as follows:
+```python
+import mashumaro.codecs.toml as toml_codec
+
+toml_codec.decode(...<shape_type>)
+toml_codec.encode(..., <shape_type>)
+```
+
+Mixin can be used as follows:
+```python
+from mashumaro.mixins.toml import DataClassTOMLMixin
+
+@dataclass
+class MyModel(DataClassTOMLMixin):
+    ...
+
+MyModel.from_toml(...)
+MyModel(...).to_toml()
+```
+
+### MessagePack
+
+[MessagePack](https://msgpack.org) is an efficient binary serialization format.
+In order to use this mixin, the [`msgpack`](https://pypi.org/project/msgpack/)
+package must be installed. You can install it manually or using an extra
+option for `mashumaro`:
+
+```shell
+pip install mashumaro[msgpack]
+```
+
+The following data types will be handled by
+[`msgpack`](https://pypi.org/project/msgpack/) library by default:
+* [`bytes`](https://docs.python.org/3/library/stdtypes.html#bytes)
+* [`bytearray`](https://docs.python.org/3/library/stdtypes.html#bytearray)
+
+Efficient decoder and encoder can be used as follows:
+```python
+from mashumaro.codecs.msgpack import MessagePackDecoder, MessagePackEncoder
+
+decoder = MessagePackDecoder(<shape_type>, ...)
+decoder.decode(...)
+
+encoder = MessagePackEncoder(<shape_type>, ...)
+encoder.encode(...)
+```
+
+Convenient functions can be used as follows:
+```python
+from mashumaro.codecs.msgpack import msgpack_decode, msgpack_encode
+
+msgpack_decode(..., <shape_type>)
+msgpack_encode(..., <shape_type>)
+```
+
+Convenient function aliases are recommended to be used as follows:
+```python
+import mashumaro.codecs.msgpack as msgpack_codec
+
+msgpack_codec.decode(...<shape_type>)
+msgpack_codec.encode(..., <shape_type>)
+```
+
+Mixin can be used as follows:
+```python
+from mashumaro.mixins.msgpack import DataClassMessagePackMixin
+
+@dataclass
+class MyModel(DataClassMessagePackMixin):
+    ...
+
+MyModel.from_msgpack(...)
+MyModel(...).to_msgpack()
+```
 
 Customization
---------------------------------------------------------------------------------
+-------------------------------------------------------------------------------
 
 Customization options of `mashumaro` are extensive and will most likely cover your needs.
 When it comes to non-standard data types and non-standard serialization support, you can do the following:
@@ -1622,6 +1889,12 @@ This dialect option has the same meaning as the
 but for the dialect scope. You can register custom [`SerializationStrategy`](#serializationstrategy),
 `serialize` and `deserialize` methods for the specific types.
 
+#### `serialize_by_alias` dialect option
+
+This dialect option has the same meaning as the
+[similar config option](#serialize_by_alias-config-option)
+but for the dialect scope.
+
 #### `omit_none` dialect option
 
 This dialect option has the same meaning as the
@@ -1632,6 +1905,12 @@ This dialect option has the same meaning as the
 This dialect option has the same meaning as the
 [similar config option](#omitdefault-config-option) but for the dialect scope.
 
+#### `namedtuple_as_dict` dialect option
+
+This dialect option has the same meaning as the
+[similar config option](#namedtuple_as_dict-config-option)
+but for the dialect scope.
+
 #### `no_copy_collections` dialect option
 
 By default, all collection data types are serialized as a copy to prevent
@@ -2526,7 +2805,7 @@ Note that you can add an additional `context` argument using the
 [corresponding](#add-context-keyword-argument) code generation option.
 
 JSON Schema
---------------------------------------------------------------------------------
+-------------------------------------------------------------------------------
 
 You can build JSON Schema not only for dataclasses but also for any other
 [supported](#supported-data-types) data
diff --git a/mashumaro/codecs/__init__.py b/mashumaro/codecs/__init__.py
new file mode 100644
index 00000000..69c322e0
--- /dev/null
+++ b/mashumaro/codecs/__init__.py
@@ -0,0 +1,6 @@
+from .basic import BasicDecoder, BasicEncoder
+
+__all__ = [
+    "BasicDecoder",
+    "BasicEncoder",
+]
diff --git a/mashumaro/codecs/_builder.py b/mashumaro/codecs/_builder.py
new file mode 100644
index 00000000..98a5ae1e
--- /dev/null
+++ b/mashumaro/codecs/_builder.py
@@ -0,0 +1,105 @@
+import re
+from typing import Any, Callable, Optional, Type
+
+from mashumaro.core.meta.code.builder import CodeBuilder
+from mashumaro.core.meta.helpers import is_optional, is_type_var_any
+from mashumaro.core.meta.types.common import (
+    AttrsHolder,
+    FieldContext,
+    ValueSpec,
+)
+from mashumaro.core.meta.types.pack import PackerRegistry
+from mashumaro.core.meta.types.unpack import UnpackerRegistry
+
+CALL_EXPR = re.compile(r"^([^ ]+)\(value\)$")
+
+
+class CodecCodeBuilder(CodeBuilder):
+    @classmethod
+    def new(cls, **kwargs: Any) -> "CodecCodeBuilder":
+        if "attrs" not in kwargs:
+            kwargs["attrs"] = AttrsHolder()
+        return cls(AttrsHolder("__root__"), **kwargs)  # type: ignore
+
+    def add_decode_method(
+        self,
+        shape_type: Type,
+        decoder_obj: Any,
+        pre_decoder_func: Optional[Callable[[Any], Any]] = None,
+    ) -> None:
+        self.reset()
+        with self.indent("def decode(value):"):
+            if pre_decoder_func:
+                self.ensure_object_imported(pre_decoder_func, "decoder")
+                self.add_line("value = decoder(value)")
+            could_be_none = (
+                shape_type in (Any, type(None), None)
+                or is_type_var_any(self._get_real_type("", shape_type))
+                or is_optional(
+                    shape_type, self.get_field_resolved_type_params("")
+                )
+            )
+            unpacked_value = UnpackerRegistry.get(
+                ValueSpec(
+                    type=shape_type,
+                    expression="value",
+                    builder=self,
+                    field_ctx=FieldContext(name="", metadata={}),
+                    could_be_none=could_be_none,
+                )
+            )
+            self.add_line(f"return {unpacked_value}")
+        self.add_line("setattr(decoder_obj, 'decode', decode)")
+        if pre_decoder_func is None:
+            m = CALL_EXPR.match(unpacked_value)
+            if m:
+                method_name = m.group(1)
+                self.lines.reset()
+                self.add_line(f"setattr(decoder_obj, 'decode', {method_name})")
+        self.ensure_object_imported(decoder_obj, "decoder_obj")
+        self.ensure_object_imported(self.cls, "cls")
+        self.compile()
+
+    def add_encode_method(
+        self,
+        shape_type: Type,
+        encoder_obj: Any,
+        post_encoder_func: Optional[Callable[[Any], Any]] = None,
+    ) -> None:
+        self.reset()
+        with self.indent("def encode(value):"):
+            could_be_none = (
+                shape_type in (Any, type(None), None)
+                or is_type_var_any(self._get_real_type("", shape_type))
+                or is_optional(
+                    shape_type, self.get_field_resolved_type_params("")
+                )
+            )
+            packed_value = PackerRegistry.get(
+                ValueSpec(
+                    type=shape_type,
+                    expression="value",
+                    builder=self,
+                    field_ctx=FieldContext(name="", metadata={}),
+                    could_be_none=could_be_none,
+                    no_copy_collections=self.get_dialect_or_config_option(
+                        "no_copy_collections", ()
+                    ),
+                )
+            )
+            if post_encoder_func:
+                self.ensure_object_imported(post_encoder_func, "encoder")
+                self.add_line(f"return encoder({packed_value})")
+            else:
+                self.add_line(f"return {packed_value}")
+        self.add_line("setattr(encoder_obj, 'encode', encode)")
+        if post_encoder_func is None:
+            m = CALL_EXPR.match(packed_value)
+            if m:
+                method_name = m.group(1)
+                self.lines.reset()
+                self.add_line(f"setattr(encoder_obj, 'encode', {method_name})")
+        self.ensure_object_imported(encoder_obj, "encoder_obj")
+        self.ensure_object_imported(self.cls, "cls")
+        self.ensure_object_imported(self.cls, "self")
+        self.compile()
diff --git a/mashumaro/codecs/basic.py b/mashumaro/codecs/basic.py
new file mode 100644
index 00000000..e40dd396
--- /dev/null
+++ b/mashumaro/codecs/basic.py
@@ -0,0 +1,109 @@
+from typing import (
+    Any,
+    Callable,
+    Generic,
+    Optional,
+    Type,
+    TypeVar,
+    Union,
+    final,
+    overload,
+)
+
+from mashumaro.codecs._builder import CodecCodeBuilder
+from mashumaro.core.meta.helpers import get_args
+from mashumaro.dialect import Dialect
+
+T = TypeVar("T")
+
+
+class BasicDecoder(Generic[T]):
+    @overload
+    def __init__(
+        self,
+        shape_type: Type[T],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        pre_decoder_func: Optional[Callable[[Any], Any]] = None,
+    ):
+        ...
+
+    @overload
+    def __init__(
+        self,
+        shape_type: Any,
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        pre_decoder_func: Optional[Callable[[Any], Any]] = None,
+    ):
+        ...
+
+    def __init__(
+        self,
+        shape_type: Union[Type[T], Any],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        pre_decoder_func: Optional[Callable[[Any], Any]] = None,
+    ):
+        code_builder = CodecCodeBuilder.new(
+            type_args=get_args(shape_type), default_dialect=default_dialect
+        )
+        code_builder.add_decode_method(shape_type, self, pre_decoder_func)
+
+    @final
+    def decode(self, data: Any) -> T:  # pragma: no cover
+        ...
+
+
+class BasicEncoder(Generic[T]):
+    @overload
+    def __init__(
+        self,
+        shape_type: Type[T],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        post_encoder_func: Optional[Callable[[Any], Any]] = None,
+    ):
+        ...
+
+    @overload
+    def __init__(
+        self,
+        shape_type: Any,
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        post_encoder_func: Optional[Callable[[Any], Any]] = None,
+    ):
+        ...
+
+    def __init__(
+        self,
+        shape_type: Union[Type[T], Any],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        post_encoder_func: Optional[Callable[[Any], Any]] = None,
+    ):
+        code_builder = CodecCodeBuilder.new(
+            type_args=get_args(shape_type), default_dialect=default_dialect
+        )
+        code_builder.add_encode_method(shape_type, self, post_encoder_func)
+
+    @final
+    def encode(self, obj: T) -> Any:  # pragma: no cover
+        ...
+
+
+def decode(data: Any, shape_type: Union[Type[T], Any]) -> T:
+    return BasicDecoder(shape_type).decode(data)
+
+
+def encode(obj: T, shape_type: Union[Type[T], Any]) -> Any:
+    return BasicEncoder(shape_type).encode(obj)
+
+
+__all__ = [
+    "BasicDecoder",
+    "BasicEncoder",
+    "decode",
+    "encode",
+]
diff --git a/mashumaro/codecs/json.py b/mashumaro/codecs/json.py
new file mode 100644
index 00000000..d0cd9677
--- /dev/null
+++ b/mashumaro/codecs/json.py
@@ -0,0 +1,129 @@
+import json
+from typing import (
+    Any,
+    Callable,
+    Generic,
+    Optional,
+    Type,
+    TypeVar,
+    Union,
+    final,
+    overload,
+)
+
+from mashumaro.codecs._builder import CodecCodeBuilder
+from mashumaro.core.meta.helpers import get_args
+from mashumaro.dialect import Dialect
+
+T = TypeVar("T")
+EncodedData = Union[str, bytes, bytearray]
+
+
+class JSONDecoder(Generic[T]):
+    @overload
+    def __init__(
+        self,
+        shape_type: Type[T],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        pre_decoder_func: Callable[[EncodedData], Any] = json.loads,
+    ):
+        ...
+
+    @overload
+    def __init__(
+        self,
+        shape_type: Any,
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        pre_decoder_func: Callable[[EncodedData], Any] = json.loads,
+    ):
+        ...
+
+    def __init__(
+        self,
+        shape_type: Union[Type[T], Any],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        pre_decoder_func: Callable[[EncodedData], Any] = json.loads,
+    ):
+        code_builder = CodecCodeBuilder.new(
+            type_args=get_args(shape_type), default_dialect=default_dialect
+        )
+        code_builder.add_decode_method(shape_type, self, pre_decoder_func)
+
+    @final
+    def decode(self, data: EncodedData) -> T:  # pragma: no cover
+        ...
+
+
+class JSONEncoder(Generic[T]):
+    @overload
+    def __init__(
+        self,
+        shape_type: Type[T],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        post_encoder_func: Callable[[Any], str] = json.dumps,
+    ):
+        ...
+
+    @overload
+    def __init__(
+        self,
+        shape_type: Any,
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        post_encoder_func: Callable[[Any], str] = json.dumps,
+    ):
+        ...
+
+    def __init__(
+        self,
+        shape_type: Union[Type[T], Any],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        post_encoder_func: Callable[[Any], str] = json.dumps,
+    ):
+        code_builder = CodecCodeBuilder.new(
+            type_args=get_args(shape_type), default_dialect=default_dialect
+        )
+        code_builder.add_encode_method(shape_type, self, post_encoder_func)
+
+    @final
+    def encode(self, obj: T) -> str:  # pragma: no cover
+        ...
+
+
+def json_decode(
+    data: EncodedData,
+    shape_type: Union[Type[T], Any],
+    pre_decoder_func: Callable[[EncodedData], Any] = json.loads,
+) -> T:
+    return JSONDecoder(shape_type, pre_decoder_func=pre_decoder_func).decode(
+        data
+    )
+
+
+def json_encode(
+    obj: T,
+    shape_type: Union[Type[T], Any],
+    post_encoder_func: Callable[[Any], str] = json.dumps,
+) -> str:
+    return JSONEncoder(shape_type, post_encoder_func=post_encoder_func).encode(
+        obj
+    )
+
+
+decode = json_decode
+encode = json_encode
+
+
+__all__ = [
+    "JSONDecoder",
+    "JSONEncoder",
+    "json_decode",
+    "json_encode",
+    "decode",
+    "encode",
+]
diff --git a/mashumaro/codecs/msgpack.py b/mashumaro/codecs/msgpack.py
new file mode 100644
index 00000000..42f8d6e4
--- /dev/null
+++ b/mashumaro/codecs/msgpack.py
@@ -0,0 +1,138 @@
+from typing import (
+    Any,
+    Callable,
+    Generic,
+    Optional,
+    Type,
+    TypeVar,
+    Union,
+    final,
+    overload,
+)
+
+import msgpack
+
+from mashumaro.codecs._builder import CodecCodeBuilder
+from mashumaro.core.meta.helpers import get_args
+from mashumaro.dialect import Dialect
+from mashumaro.mixins.msgpack import MessagePackDialect
+
+T = TypeVar("T")
+
+EncodedData = bytes
+PostEncoderFunc = Callable[[Any], EncodedData]
+PreDecoderFunc = Callable[[EncodedData], Any]
+
+
+def _default_decoder(data: EncodedData) -> Any:
+    return msgpack.unpackb(data, raw=False)
+
+
+def _default_encoder(data: Any) -> EncodedData:
+    return msgpack.packb(data, use_bin_type=True)
+
+
+class MessagePackDecoder(Generic[T]):
+    @overload
+    def __init__(
+        self,
+        shape_type: Type[T],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        pre_decoder_func: Optional[PreDecoderFunc] = _default_decoder,
+    ):
+        ...
+
+    @overload
+    def __init__(
+        self,
+        shape_type: Any,
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        pre_decoder_func: Optional[PreDecoderFunc] = _default_decoder,
+    ):
+        ...
+
+    def __init__(
+        self,
+        shape_type: Union[Type[T], Any],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        pre_decoder_func: Optional[PreDecoderFunc] = _default_decoder,
+    ):
+        if default_dialect is not None:
+            default_dialect = MessagePackDialect.merge(default_dialect)
+        else:
+            default_dialect = MessagePackDialect
+        code_builder = CodecCodeBuilder.new(
+            type_args=get_args(shape_type), default_dialect=default_dialect
+        )
+        code_builder.add_decode_method(shape_type, self, pre_decoder_func)
+
+    @final
+    def decode(self, data: EncodedData) -> T:  # pragma: no cover
+        ...
+
+
+class MessagePackEncoder(Generic[T]):
+    @overload
+    def __init__(
+        self,
+        shape_type: Type[T],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        post_encoder_func: Optional[PostEncoderFunc] = _default_encoder,
+    ):
+        ...
+
+    @overload
+    def __init__(
+        self,
+        shape_type: Any,
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        post_encoder_func: Optional[PostEncoderFunc] = _default_encoder,
+    ):
+        ...
+
+    def __init__(
+        self,
+        shape_type: Union[Type[T], Any],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        post_encoder_func: Optional[PostEncoderFunc] = _default_encoder,
+    ):
+        if default_dialect is not None:
+            default_dialect = MessagePackDialect.merge(default_dialect)
+        else:
+            default_dialect = MessagePackDialect
+        code_builder = CodecCodeBuilder.new(
+            type_args=get_args(shape_type), default_dialect=default_dialect
+        )
+        code_builder.add_encode_method(shape_type, self, post_encoder_func)
+
+    @final
+    def encode(self, obj: T) -> EncodedData:  # pragma: no cover
+        ...
+
+
+def msgpack_decode(data: EncodedData, shape_type: Union[Type[T], Any]) -> T:
+    return MessagePackDecoder(shape_type).decode(data)
+
+
+def msgpack_encode(obj: T, shape_type: Union[Type[T], Any]) -> EncodedData:
+    return MessagePackEncoder(shape_type).encode(obj)
+
+
+decode = msgpack_decode
+encode = msgpack_encode
+
+
+__all__ = [
+    "MessagePackDecoder",
+    "MessagePackEncoder",
+    "msgpack_decode",
+    "msgpack_encode",
+    "decode",
+    "encode",
+]
diff --git a/mashumaro/codecs/orjson.py b/mashumaro/codecs/orjson.py
new file mode 100644
index 00000000..d4e1b89b
--- /dev/null
+++ b/mashumaro/codecs/orjson.py
@@ -0,0 +1,120 @@
+from typing import (
+    Any,
+    Generic,
+    Optional,
+    Type,
+    TypeVar,
+    Union,
+    final,
+    overload,
+)
+
+import orjson
+
+from mashumaro.codecs._builder import CodecCodeBuilder
+from mashumaro.core.meta.helpers import get_args
+from mashumaro.dialect import Dialect
+from mashumaro.mixins.orjson import OrjsonDialect
+
+T = TypeVar("T")
+EncodedData = Union[str, bytes, bytearray]
+
+
+class ORJSONDecoder(Generic[T]):
+    @overload
+    def __init__(
+        self,
+        shape_type: Type[T],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+    ):
+        ...
+
+    @overload
+    def __init__(
+        self,
+        shape_type: Any,
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+    ):
+        ...
+
+    def __init__(
+        self,
+        shape_type: Union[Type[T], Any],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+    ):
+        if default_dialect is not None:
+            default_dialect = OrjsonDialect.merge(default_dialect)
+        else:
+            default_dialect = OrjsonDialect
+        code_builder = CodecCodeBuilder.new(
+            type_args=get_args(shape_type), default_dialect=default_dialect
+        )
+        code_builder.add_decode_method(shape_type, self, orjson.loads)
+
+    @final
+    def decode(self, data: EncodedData) -> T:  # pragma: no cover
+        ...
+
+
+class ORJSONEncoder(Generic[T]):
+    @overload
+    def __init__(
+        self,
+        shape_type: Type[T],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+    ):
+        ...
+
+    @overload
+    def __init__(
+        self,
+        shape_type: Any,
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+    ):
+        ...
+
+    def __init__(
+        self,
+        shape_type: Union[Type[T], Any],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+    ):
+        if default_dialect is not None:
+            default_dialect = OrjsonDialect.merge(default_dialect)
+        else:
+            default_dialect = OrjsonDialect
+        code_builder = CodecCodeBuilder.new(
+            type_args=get_args(shape_type), default_dialect=default_dialect
+        )
+        code_builder.add_encode_method(shape_type, self, orjson.dumps)
+
+    @final
+    def encode(self, obj: T) -> bytes:  # pragma: no cover
+        ...
+
+
+def json_decode(data: EncodedData, shape_type: Type[T]) -> T:
+    return ORJSONDecoder(shape_type).decode(data)
+
+
+def json_encode(obj: T, shape_type: Union[Type[T], Any]) -> bytes:
+    return ORJSONEncoder(shape_type).encode(obj)
+
+
+decode = json_decode
+encode = json_encode
+
+
+__all__ = [
+    "ORJSONDecoder",
+    "ORJSONEncoder",
+    "json_decode",
+    "json_encode",
+    "decode",
+    "encode",
+]
diff --git a/mashumaro/codecs/toml.py b/mashumaro/codecs/toml.py
new file mode 100644
index 00000000..2b82be67
--- /dev/null
+++ b/mashumaro/codecs/toml.py
@@ -0,0 +1,125 @@
+from typing import (
+    Any,
+    Generic,
+    Optional,
+    Type,
+    TypeVar,
+    Union,
+    final,
+    overload,
+)
+
+import tomli_w
+
+from mashumaro.codecs._builder import CodecCodeBuilder
+from mashumaro.core.meta.helpers import get_args
+from mashumaro.dialect import Dialect
+from mashumaro.mixins.toml import TOMLDialect
+
+try:
+    import tomllib
+except ModuleNotFoundError:
+    import tomli as tomllib  # type: ignore
+
+T = TypeVar("T")
+EncodedData = str
+
+
+class TOMLDecoder(Generic[T]):
+    @overload
+    def __init__(
+        self,
+        shape_type: Type[T],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+    ):
+        ...
+
+    @overload
+    def __init__(
+        self,
+        shape_type: Any,
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+    ):
+        ...
+
+    def __init__(
+        self,
+        shape_type: Union[Type[T], Any],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+    ):
+        if default_dialect is not None:
+            default_dialect = TOMLDialect.merge(default_dialect)
+        else:
+            default_dialect = TOMLDialect
+        code_builder = CodecCodeBuilder.new(
+            type_args=get_args(shape_type), default_dialect=default_dialect
+        )
+        code_builder.add_decode_method(shape_type, self, tomllib.loads)
+
+    @final
+    def decode(self, data: EncodedData) -> T:  # pragma: no cover
+        ...
+
+
+class TOMLEncoder(Generic[T]):
+    @overload
+    def __init__(
+        self,
+        shape_type: Type[T],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+    ):
+        ...
+
+    @overload
+    def __init__(
+        self,
+        shape_type: Any,
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+    ):
+        ...
+
+    def __init__(
+        self,
+        shape_type: Union[Type[T], Any],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+    ):
+        if default_dialect is not None:
+            default_dialect = TOMLDialect.merge(default_dialect)
+        else:
+            default_dialect = TOMLDialect
+        code_builder = CodecCodeBuilder.new(
+            type_args=get_args(shape_type), default_dialect=default_dialect
+        )
+        code_builder.add_encode_method(shape_type, self, tomli_w.dumps)
+
+    @final
+    def encode(self, obj: T) -> bytes:  # pragma: no cover
+        ...
+
+
+def toml_decode(data: EncodedData, shape_type: Type[T]) -> T:
+    return TOMLDecoder(shape_type).decode(data)
+
+
+def toml_encode(obj: T, shape_type: Union[Type[T], Any]) -> bytes:
+    return TOMLEncoder(shape_type).encode(obj)
+
+
+decode = toml_decode
+encode = toml_encode
+
+
+__all__ = [
+    "TOMLDecoder",
+    "TOMLEncoder",
+    "toml_decode",
+    "toml_encode",
+    "decode",
+    "encode",
+]
diff --git a/mashumaro/codecs/yaml.py b/mashumaro/codecs/yaml.py
new file mode 100644
index 00000000..805a9fb1
--- /dev/null
+++ b/mashumaro/codecs/yaml.py
@@ -0,0 +1,134 @@
+from typing import (
+    Any,
+    Callable,
+    Generic,
+    Optional,
+    Type,
+    TypeVar,
+    Union,
+    final,
+    overload,
+)
+
+import yaml
+
+from mashumaro.codecs._builder import CodecCodeBuilder
+from mashumaro.core.meta.helpers import get_args
+from mashumaro.dialect import Dialect
+
+T = TypeVar("T")
+
+
+EncodedData = Union[str, bytes]
+PostEncoderFunc = Callable[[Any], EncodedData]
+PreDecoderFunc = Callable[[EncodedData], Any]
+
+
+DefaultLoader = getattr(yaml, "CSafeLoader", yaml.SafeLoader)
+DefaultDumper = getattr(yaml, "CDumper", yaml.Dumper)
+
+
+def _default_encoder(data: Any) -> EncodedData:
+    return yaml.dump(data, Dumper=DefaultDumper)
+
+
+def _default_decoder(data: EncodedData) -> Any:
+    return yaml.load(data, DefaultLoader)
+
+
+class YAMLDecoder(Generic[T]):
+    @overload
+    def __init__(
+        self,
+        shape_type: Type[T],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        pre_decoder_func: Optional[PreDecoderFunc] = _default_decoder,
+    ):
+        ...
+
+    @overload
+    def __init__(
+        self,
+        shape_type: Any,
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        pre_decoder_func: Optional[PreDecoderFunc] = _default_decoder,
+    ):
+        ...
+
+    def __init__(
+        self,
+        shape_type: Union[Type[T], Any],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        pre_decoder_func: Optional[PreDecoderFunc] = _default_decoder,
+    ):
+        code_builder = CodecCodeBuilder.new(
+            type_args=get_args(shape_type), default_dialect=default_dialect
+        )
+        code_builder.add_decode_method(shape_type, self, pre_decoder_func)
+
+    @final
+    def decode(self, data: EncodedData) -> T:  # pragma: no cover
+        ...
+
+
+class YAMLEncoder(Generic[T]):
+    @overload
+    def __init__(
+        self,
+        shape_type: Type[T],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        post_encoder_func: Optional[PostEncoderFunc] = _default_encoder,
+    ):
+        ...
+
+    @overload
+    def __init__(
+        self,
+        shape_type: Any,
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        post_encoder_func: Optional[PostEncoderFunc] = _default_encoder,
+    ):
+        ...
+
+    def __init__(
+        self,
+        shape_type: Union[Type[T], Any],
+        *,
+        default_dialect: Optional[Type[Dialect]] = None,
+        post_encoder_func: Optional[PostEncoderFunc] = _default_encoder,
+    ):
+        code_builder = CodecCodeBuilder.new(
+            type_args=get_args(shape_type), default_dialect=default_dialect
+        )
+        code_builder.add_encode_method(shape_type, self, post_encoder_func)
+
+    @final
+    def encode(self, obj: T) -> EncodedData:  # pragma: no cover
+        ...
+
+
+def yaml_decode(data: EncodedData, shape_type: Union[Type[T], Any]) -> T:
+    return YAMLDecoder(shape_type).decode(data)
+
+
+def yaml_encode(obj: T, shape_type: Union[Type[T], Any]) -> EncodedData:
+    return YAMLEncoder(shape_type).encode(obj)
+
+
+decode = yaml_decode
+encode = yaml_encode
+
+
+__all__ = [
+    "YAMLDecoder",
+    "YAMLEncoder",
+    "yaml_decode",
+    "yaml_encode",
+    "decode",
+    "encode",
+]
diff --git a/mashumaro/config.py b/mashumaro/config.py
index e797f1cc..8ad670ca 100644
--- a/mashumaro/config.py
+++ b/mashumaro/config.py
@@ -38,8 +38,12 @@ class BaseConfig:
     code_generation_options: List[CodeGenerationOption] = []  # type: ignore
     serialization_strategy: Dict[Any, SerializationStrategyValueType] = {}
     aliases: Dict[str, str] = {}
-    serialize_by_alias: bool = False
-    namedtuple_as_dict: bool = False
+    serialize_by_alias: Union[
+        bool, Literal[Sentinel.MISSING]
+    ] = Sentinel.MISSING
+    namedtuple_as_dict: Union[
+        bool, Literal[Sentinel.MISSING]
+    ] = Sentinel.MISSING
     allow_postponed_evaluation: bool = True
     dialect: Optional[Type[Dialect]] = None
     omit_none: Union[bool, Literal[Sentinel.MISSING]] = Sentinel.MISSING
diff --git a/mashumaro/core/meta/code/builder.py b/mashumaro/core/meta/code/builder.py
index 7c875ead..e067e7f0 100644
--- a/mashumaro/core/meta/code/builder.py
+++ b/mashumaro/core/meta/code/builder.py
@@ -107,6 +107,10 @@ def __init__(
         encoder: typing.Optional[typing.Any] = None,
         encoder_kwargs: typing.Optional[typing.Dict[str, typing.Any]] = None,
         default_dialect: typing.Optional[typing.Type[Dialect]] = None,
+        attrs: typing.Any = None,
+        attrs_registry: typing.Optional[
+            typing.Dict[typing.Any, typing.Any]
+        ] = None,
     ):
         self.cls = cls
         self.lines: CodeLines = CodeLines()
@@ -129,6 +133,15 @@ def __init__(
         self.encoder = encoder
         self.encoder_kwargs = encoder_kwargs or {}
 
+        if attrs is not None:
+            self.attrs = attrs
+        else:
+            self.attrs = cls
+        if attrs_registry is not None:
+            self.attrs_registry = attrs_registry
+        else:
+            self.attrs_registry = {}
+
     def reset(self) -> None:
         self.lines.reset()
         self.globals = globals().copy()
@@ -145,6 +158,10 @@ def namespace(self) -> typing.Mapping[typing.Any, typing.Any]:
     def annotations(self) -> typing.Dict[str, typing.Any]:
         return self.namespace.get("__annotations__", {})
 
+    @property
+    def is_nailed(self) -> bool:
+        return self.attrs is self.cls
+
     def __get_field_types(
         self, recursive: bool = True, include_extras: bool = False
     ) -> typing.Dict[str, typing.Any]:
@@ -335,7 +352,11 @@ def _add_unpack_method_lines_lazy(self, method_name: str) -> None:
 
     def _add_unpack_method_lines(self, method_name: str) -> None:
         config = self.get_config()
-        if config.lazy_compilation and self.allow_postponed_evaluation:
+        if (
+            config.lazy_compilation
+            and self.allow_postponed_evaluation
+            and self.is_nailed
+        ):
             self._add_unpack_method_lines_lazy(method_name)
             return
         try:
@@ -432,7 +453,7 @@ def _add_unpack_method_lines(self, method_name: str) -> None:
                         self._unpack_method_set_value(
                             fname, ftype, metadata, alias=alias
                         )
-                with self.indent("except TypeError:"):
+                with self.indent("except AttributeError:"):
                     with self.indent("if not isinstance(d, dict):"):
                         self.add_line(
                             f"raise ValueError('Argument for "
@@ -493,7 +514,7 @@ def add_unpack_method(self) -> None:
             with self.indent(f"if not '{cache_name}' in cls.__dict__:"):
                 self.add_line(f"cls.{cache_name} = {{}}")
 
-        if self.dialect is None:
+        if self.dialect is None and self.is_nailed:
             self.add_line("@classmethod")
         self._add_unpack_method_definition(method_name)
         with self.indent():
@@ -504,14 +525,7 @@ def add_unpack_method(self) -> None:
                     self._add_unpack_method_with_dialect_lines(method_name)
             else:
                 self._add_unpack_method_lines(method_name)
-        if self.dialect is None:
-            self.add_line(f"setattr(cls, '{method_name}', {method_name})")
-            if is_dataclass_dict_mixin_subclass(self.cls):
-                self.add_line(
-                    f"setattr(cls, '{method_name.public}', {method_name})"
-                )
-        else:
-            self.add_line(f"cls.{cache_name}[dialect] = {method_name}")
+        self._add_setattr_method(method_name, cache_name)
         self.compile()
 
     def _add_unpack_method_definition(self, method_name: str) -> None:
@@ -521,7 +535,11 @@ def _add_unpack_method_definition(self, method_name: str) -> None:
         )
         if default_kwargs:
             kwargs += f", {default_kwargs}"
-        self.add_line(f"def {method_name}(cls, d{kwargs}):")
+
+        if self.is_nailed:
+            self.add_line(f"def {method_name}(cls, d{kwargs}):")
+        else:
+            self.add_line(f"def {method_name}(d{kwargs}):")
 
     def _unpack_method_set_value(
         self,
@@ -721,7 +739,7 @@ def get_pack_method_default_flag_values(
             TO_DICT_ADD_OMIT_NONE_FLAG, cls
         )
         if omit_none_feature:
-            omit_none = self._get_dialect_or_config_option("omit_none", False)
+            omit_none = self.get_dialect_or_config_option("omit_none", False)
             kw_param_names.append("omit_none")
             kw_param_values.append("True" if omit_none else "False")
 
@@ -729,7 +747,9 @@ def get_pack_method_default_flag_values(
             TO_DICT_ADD_BY_ALIAS_FLAG, cls
         )
         if by_alias_feature:
-            serialize_by_alias = self.get_config(cls).serialize_by_alias
+            serialize_by_alias = self.get_dialect_or_config_option(
+                "serialize_by_alias", False, cls
+            )
             kw_param_names.append("by_alias")
             kw_param_values.append("True" if serialize_by_alias else "False")
 
@@ -852,7 +872,11 @@ def _add_pack_method_lines_lazy(self, method_name: str) -> None:
 
     def _add_pack_method_lines(self, method_name: str) -> None:
         config = self.get_config()
-        if config.lazy_compilation and self.allow_postponed_evaluation:
+        if (
+            config.lazy_compilation
+            and self.allow_postponed_evaluation
+            and self.is_nailed
+        ):
             self._add_pack_method_lines_lazy(method_name)
             return
         try:
@@ -882,9 +906,11 @@ def _add_pack_method_lines(self, method_name: str) -> None:
             omit_none_feature = self.is_code_generation_option_enabled(
                 TO_DICT_ADD_OMIT_NONE_FLAG
             )
-            serialize_by_alias = self.get_config().serialize_by_alias
-            omit_none = self._get_dialect_or_config_option("omit_none", False)
-            omit_default = self._get_dialect_or_config_option(
+            serialize_by_alias = self.get_dialect_or_config_option(
+                "serialize_by_alias", False
+            )
+            omit_none = self.get_dialect_or_config_option("omit_none", False)
+            omit_default = self.get_dialect_or_config_option(
                 "omit_default", False
             )
             force_value = omit_default
@@ -1074,7 +1100,9 @@ def __pack_method_set_value(
             with self.indent("else:"):
                 self.add_line(f"kwargs['{fname}'] = {packed_value}")
         else:
-            serialize_by_alias = self.get_config().serialize_by_alias
+            serialize_by_alias = self.get_dialect_or_config_option(
+                "serialize_by_alias", False
+            )
             if serialize_by_alias and alias is not None:
                 fname_or_alias = alias
             else:
@@ -1157,15 +1185,25 @@ def add_pack_method(self) -> None:
                     self._add_pack_method_with_dialect_lines(method_name)
             else:
                 self._add_pack_method_lines(method_name)
+        self._add_setattr_method(method_name, cache_name)
+        self.compile()
+
+    def _add_setattr_method(
+        self, method_name: InternalMethodName, cache_name: str
+    ) -> None:
         if self.dialect is None:
-            self.add_line(f"setattr(cls, '{method_name}', {method_name})")
-            if is_dataclass_dict_mixin_subclass(self.cls):
-                self.add_line(
-                    f"setattr(cls, '{method_name.public}', {method_name})"
-                )
+            if not self.is_nailed:
+                self.ensure_object_imported(self.attrs, "_cls")
+                self.ensure_object_imported(self.cls, "cls")
+                self.add_line(f"setattr(_cls, '{method_name}', {method_name})")
+            else:
+                self.add_line(f"setattr(cls, '{method_name}', {method_name})")
+                if is_dataclass_dict_mixin_subclass(self.cls):
+                    self.add_line(
+                        f"setattr(cls, '{method_name.public}', {method_name})"
+                    )
         else:
             self.add_line(f"cls.{cache_name}[dialect] = {method_name}")
-        self.compile()
 
     def _get_field_packer(
         self,
@@ -1195,7 +1233,7 @@ def _get_field_packer(
                     metadata=metadata,
                 ),
                 could_be_none=False,
-                no_copy_collections=self._get_dialect_or_config_option(
+                no_copy_collections=self.get_dialect_or_config_option(
                     "no_copy_collections", ()
                 ),
             )
@@ -1228,7 +1266,7 @@ def __iter_serialization_strategies(
         if self.default_dialect is not None:
             yield self.default_dialect.serialization_strategy.get(ftype)
 
-    def _get_dialect_or_config_option(
+    def get_dialect_or_config_option(
         self,
         option: str,
         default: typing.Any,
diff --git a/mashumaro/core/meta/helpers.py b/mashumaro/core/meta/helpers.py
index 788bd0d4..2d27c44d 100644
--- a/mashumaro/core/meta/helpers.py
+++ b/mashumaro/core/meta/helpers.py
@@ -80,6 +80,7 @@
     "hash_type_args",
     "iter_all_subclasses",
     "is_hashable",
+    "is_hashable_type",
     "evaluate_forward_ref",
 ]
 
@@ -167,7 +168,7 @@ def _get_literal_values_str(typ: Type, short: bool) -> str:
     for value in get_literal_values(typ):
         if isinstance(value, enum.Enum):
             values_str.append(f"{type_name(type(value), short)}.{value.name}")
-        elif isinstance(  # type: ignore
+        elif isinstance(
             value, (int, str, bytes, bool, NoneType)  # type: ignore
         ):
             values_str.append(repr(value))
diff --git a/mashumaro/core/meta/types/common.py b/mashumaro/core/meta/types/common.py
index a8ceb60a..c5a1e282 100644
--- a/mashumaro/core/meta/types/common.py
+++ b/mashumaro/core/meta/types/common.py
@@ -1,7 +1,9 @@
 import collections.abc
 import uuid
+from abc import ABC, abstractmethod
 from dataclasses import dataclass, field, replace
 from functools import cached_property
+from types import new_class
 from typing import (
     TYPE_CHECKING,
     Any,
@@ -18,17 +20,19 @@
 from typing_extensions import ParamSpec, TypeAlias
 
 from mashumaro.core.const import PEP_585_COMPATIBLE
+from mashumaro.core.meta.code.lines import CodeLines
 from mashumaro.core.meta.helpers import (
     get_args,
     get_type_origin,
     is_annotated,
     is_generic,
     is_hashable_type,
+    is_self,
     type_name,
 )
 from mashumaro.exceptions import UnserializableField
 
-if TYPE_CHECKING:  # pragma no cover
+if TYPE_CHECKING:  # pragma: no cover
     from mashumaro.core.meta.code.builder import CodeBuilder
 else:
     CodeBuilder = Any
@@ -41,6 +45,18 @@
 T = TypeVar("T")
 
 
+class AttrsHolder:
+    def __new__(
+        cls, name: Optional[str] = None, *args: Any, **kwargs: Any
+    ) -> Any:
+        ah = new_class("AttrsHolder")
+        ah_id = id(ah)
+        if not name:
+            name = f"attrs_{ah_id}"
+        ah.__name__ = ah.__qualname__ = name
+        return ah
+
+
 class ExpressionWrapper:
     def __init__(self, expression: str):
         self.expression = expression
@@ -93,6 +109,111 @@ def copy(self, **changes: Any) -> "ValueSpec":
     def annotations(self) -> Sequence[str]:
         return getattr(self.annotated_type, "__metadata__", [])
 
+    @cached_property
+    def attrs(self) -> Any:
+        if self.builder.is_nailed:
+            return self.builder.attrs
+        if is_self(self.type):
+            typ = self.builder.cls
+        else:
+            typ = self.origin_type
+        attrs = self.attrs_registry.get(typ)
+        if attrs is None:
+            attrs = AttrsHolder()
+            self.attrs_registry[typ] = attrs
+        return attrs
+
+    @cached_property
+    def cls_attrs_name(self) -> str:
+        if self.builder.is_nailed:
+            return "cls"
+        else:
+            self.builder.ensure_object_imported(self.attrs)
+            return self.attrs.__name__
+
+    @cached_property
+    def self_attrs_name(self) -> str:
+        if self.builder.is_nailed:
+            return "self"
+        else:
+            self.builder.ensure_object_imported(self.attrs)
+            return self.attrs.__name__
+
+    @cached_property
+    def attrs_registry(self) -> Dict[Any, Any]:
+        return self.builder.attrs_registry
+
+    @cached_property
+    def attrs_registry_name(self) -> str:
+        name = f"attrs_registry_{id(self.attrs_registry)}"
+        self.builder.ensure_object_imported(self.attrs_registry, name)
+        return name
+
+
+class AbstractMethodBuilder(ABC):
+    @abstractmethod
+    def get_method_prefix(self) -> str:  # pragma: no cover
+        raise NotImplementedError
+
+    def _generate_method_name(
+        self, spec: ValueSpec
+    ) -> str:  # pragma: no cover
+        prefix = self.get_method_prefix()
+        if prefix:
+            prefix = f"{prefix}_"
+        if spec.field_ctx.name:
+            suffix = f"_{spec.field_ctx.name}"
+        else:
+            suffix = ""
+        return (
+            f"__{prefix}{spec.builder.cls.__name__}{suffix}"
+            f"__{random_hex()}"
+        )
+
+    @abstractmethod
+    def _add_definition(self, spec: ValueSpec, lines: CodeLines) -> str:
+        raise NotImplementedError
+
+    @abstractmethod
+    def _generate_method_args(self, spec: ValueSpec) -> str:
+        raise NotImplementedError
+
+    @abstractmethod
+    def _add_body(
+        self, spec: ValueSpec, lines: CodeLines
+    ) -> None:  # pragma: no cover
+        raise NotImplementedError
+
+    def _add_setattr(
+        self, spec: ValueSpec, method_name: str, lines: CodeLines
+    ) -> None:
+        lines.append(
+            f"setattr({spec.cls_attrs_name}, '{method_name}', {method_name})"
+        )
+
+    def _compile(self, spec: ValueSpec, lines: CodeLines) -> None:
+        if spec.builder.get_config().debug:
+            print(f"{type_name(spec.builder.cls)}:")
+            print(lines.as_text())
+        exec(lines.as_text(), spec.builder.globals, spec.builder.__dict__)
+
+    @abstractmethod
+    def _get_call_expr(self, spec: ValueSpec, method_name: str) -> str:
+        raise NotImplementedError
+
+    def _before_build(self, spec: ValueSpec) -> None:
+        pass
+
+    def build(self, spec: ValueSpec) -> str:
+        self._before_build(spec)
+        lines = CodeLines()
+        method_name = self._add_definition(spec, lines)
+        with lines.indent():
+            self._add_body(spec, lines)
+        self._add_setattr(spec, method_name, lines)
+        self._compile(spec, lines)
+        return self._get_call_expr(spec, method_name)
+
 
 ValueSpecExprCreator: TypeAlias = Callable[[ValueSpec], Optional[Expression]]
 
@@ -183,4 +304,6 @@ def random_hex() -> str:
 
 
 def clean_id(value: str) -> str:
-    return value.replace(".", "_")
+    for c in ".<>":
+        value = value.replace(c, "_")
+    return value
diff --git a/mashumaro/core/meta/types/pack.py b/mashumaro/core/meta/types/pack.py
index 0b16d229..21faf605 100644
--- a/mashumaro/core/meta/types/pack.py
+++ b/mashumaro/core/meta/types/pack.py
@@ -56,6 +56,7 @@
     NoneType,
     Registry,
     ValueSpec,
+    clean_id,
     ensure_generic_collection,
     ensure_generic_collection_subclass,
     ensure_generic_mapping,
@@ -105,11 +106,13 @@ def _pack_with_annotated_serialization_strategy(
         resolve_type_params(strategy_type, get_args(spec.type))[strategy_type],
     )
     overridden_fn = f"__{spec.field_ctx.name}_serialize_{random_hex()}"
-    setattr(spec.builder.cls, overridden_fn, strategy.serialize)
+    setattr(spec.attrs, overridden_fn, strategy.serialize)
     return PackerRegistry.get(
         spec.copy(
             type=value_type,
-            expression=f"self.{overridden_fn}({spec.expression})",
+            expression=(
+                f"{spec.self_attrs_name}.{overridden_fn}({spec.expression})"
+            ),
         )
     )
 
@@ -156,10 +159,8 @@ def pack_type_with_overridden_serialization(
         return serialization_method.expression
     elif callable(serialization_method):
         overridden_fn = f"__{spec.field_ctx.name}_serialize_{random_hex()}"
-        setattr(
-            spec.builder.cls, overridden_fn, staticmethod(serialization_method)
-        )
-        return f"self.{overridden_fn}({spec.expression})"
+        setattr(spec.attrs, overridden_fn, staticmethod(serialization_method))
+        return f"{spec.self_attrs_name}.{overridden_fn}({spec.expression})"
 
 
 def _pack_annotated_serializable_type(
@@ -228,9 +229,10 @@ def pack_dataclass(spec: ValueSpec) -> Optional[Expression]:
         method_name = spec.builder.get_pack_method_name(
             type_args, spec.builder.format_name
         )
+        method_loc = spec.origin_type if spec.builder.is_nailed else spec.attrs
         if get_class_that_defines_method(
-            method_name, spec.origin_type
-        ) != spec.origin_type and (
+            method_name, method_loc
+        ) != method_loc and (
             spec.origin_type != spec.builder.cls
             or spec.builder.get_pack_method_name(
                 type_args=type_args,
@@ -245,10 +247,23 @@ def pack_dataclass(spec: ValueSpec) -> Optional[Expression]:
                 dialect=spec.builder.dialect,
                 format_name=spec.builder.format_name,
                 default_dialect=spec.builder.default_dialect,
+                attrs=method_loc,
+                attrs_registry=(
+                    spec.attrs_registry if not spec.builder.is_nailed else None
+                ),
             )
             builder.add_pack_method()
         flags = spec.builder.get_pack_method_flags(spec.type)
-        return f"{spec.expression}.{method_name}({flags})"
+        if spec.builder.is_nailed:
+            return f"{spec.expression}.{method_name}({flags})"
+        else:
+            cls_alias = clean_id(type_name(spec.origin_type))
+            method_name_alias = f"{cls_alias}_{method_name}"
+            spec.builder.ensure_object_imported(
+                getattr(spec.attrs, method_name), method_name_alias
+            )
+            method_args = spec.expression
+            return f"{method_name_alias}({method_args})"
 
 
 @register
@@ -271,11 +286,12 @@ def pack_union(
         f"__pack_{prefix}_{spec.builder.cls.__name__}_{spec.field_ctx.name}__"
         f"{random_hex()}"
     )
+    method_args = "self, value" if spec.builder.is_nailed else "value"
     default_kwargs = spec.builder.get_pack_method_default_flag_values()
     if default_kwargs:
-        lines.append(f"def {method_name}(self, value, {default_kwargs}):")
+        lines.append(f"def {method_name}({method_args}, {default_kwargs}):")
     else:
-        lines.append(f"def {method_name}(self, value):")
+        lines.append(f"def {method_name}({method_args}):")
     with lines.indent():
         for packer in (
             PackerRegistry.get(spec.copy(type=type_arg, expression="value"))
@@ -290,11 +306,16 @@ def pack_union(
                 spec.field_ctx.name
             ),
         )
-        lines.append(
-            f"raise InvalidFieldValue('{spec.field_ctx.name}',{field_type},"
-            f"value,type(self))"
-        )
-    lines.append(f"setattr(cls, '{method_name}', {method_name})")
+        if spec.builder.is_nailed:
+            lines.append(
+                f"raise InvalidFieldValue("
+                f"'{spec.field_ctx.name}',{field_type},value,type(self))"
+            )
+        else:
+            lines.append("raise ValueError(value)")
+    lines.append(
+        f"setattr({spec.cls_attrs_name}, '{method_name}', {method_name})"
+    )
     if spec.builder.get_config().debug:
         print(f"{type_name(spec.builder.cls)}:")
         print(lines.as_text())
@@ -302,7 +323,13 @@ def pack_union(
     method_args = ", ".join(
         filter(None, (spec.expression, spec.builder.get_pack_method_flags()))
     )
-    return f"self.{method_name}({method_args})"
+    if spec.builder.is_nailed:
+        return f"{spec.self_attrs_name}.{method_name}({method_args})"
+    else:
+        spec.builder.ensure_object_imported(
+            getattr(spec.attrs, method_name), method_name
+        )
+        return f"{method_name}({method_args})"
 
 
 def pack_literal(spec: ValueSpec) -> Expression:
@@ -312,11 +339,12 @@ def pack_literal(spec: ValueSpec) -> Expression:
         f"__pack_literal_{spec.builder.cls.__name__}_{spec.field_ctx.name}__"
         f"{random_hex()}"
     )
+    method_args = "self, value" if spec.builder.is_nailed else "value"
     default_kwargs = spec.builder.get_pack_method_default_flag_values()
     if default_kwargs:
-        lines.append(f"def {method_name}(self, value, {default_kwargs}):")
+        lines.append(f"def {method_name}({method_args}, {default_kwargs}):")
     else:
-        lines.append(f"def {method_name}(self, value):")
+        lines.append(f"def {method_name}({method_args}):")
     resolved_type_params = spec.builder.get_field_resolved_type_params(
         spec.field_ctx.name
     )
@@ -345,11 +373,16 @@ def pack_literal(spec: ValueSpec) -> Expression:
             typ=spec.type,
             resolved_type_params=resolved_type_params,
         )
-        lines.append(
-            f"raise InvalidFieldValue('{spec.field_ctx.name}',"
-            f"{field_type},value,type(self))"
-        )
-    lines.append(f"setattr(cls, '{method_name}', {method_name})")
+        if spec.builder.is_nailed:
+            lines.append(
+                f"raise InvalidFieldValue('{spec.field_ctx.name}',"
+                f"{field_type},value,type(self))"
+            )
+        else:
+            lines.append("raise ValueError(value)")
+    lines.append(
+        f"setattr({spec.cls_attrs_name}, '{method_name}', {method_name})"
+    )
     if spec.builder.get_config().debug:
         print(f"{type_name(spec.builder.cls)}:")
         print(lines.as_text())
@@ -357,7 +390,7 @@ def pack_literal(spec: ValueSpec) -> Expression:
     method_args = ", ".join(
         filter(None, (spec.expression, spec.builder.get_pack_method_flags()))
     )
-    return f"self.{method_name}({method_args})"
+    return f"{spec.self_attrs_name}.{method_name}({method_args})"
 
 
 @register
@@ -400,9 +433,12 @@ def pack_special_typing_primitive(spec: ValueSpec) -> Optional[Expression]:
             method_name = spec.builder.get_pack_method_name(
                 format_name=spec.builder.format_name
             )
+            method_loc = (
+                spec.builder.cls if spec.builder.is_nailed else spec.attrs
+            )
             if (
-                get_class_that_defines_method(method_name, spec.builder.cls)
-                != spec.builder.cls
+                get_class_that_defines_method(method_name, method_loc)
+                != method_loc
                 # not hasattr(self.cls, method_name)
                 and spec.builder.get_pack_method_name(
                     format_name=spec.builder.format_name,
@@ -415,10 +451,20 @@ def pack_special_typing_primitive(spec: ValueSpec) -> Optional[Expression]:
                     dialect=spec.builder.dialect,
                     format_name=spec.builder.format_name,
                     default_dialect=spec.builder.default_dialect,
+                    attrs=method_loc,
+                    attrs_registry=(
+                        spec.attrs_registry
+                        if not spec.builder.is_nailed
+                        else None
+                    ),
                 )
                 builder.add_pack_method()
             flags = spec.builder.get_pack_method_flags(spec.builder.cls)
-            return f"{spec.expression}.{method_name}({flags})"
+            if spec.builder.is_nailed:
+                return f"{spec.expression}.{method_name}({flags})"
+            else:
+                method_args = spec.expression
+                return f"_cls.{method_name}({method_args})"
         elif is_required(spec.type) or is_not_required(spec.type):
             return PackerRegistry.get(spec.copy(type=get_args(spec.type)[0]))
         elif is_unpack(spec.type):
@@ -568,7 +614,9 @@ def pack_named_tuple(spec: ValueSpec) -> Expression:
     }
     fields = getattr(spec.type, "_fields", ())
     packers = []
-    as_dict = spec.builder.get_config().namedtuple_as_dict
+    as_dict = spec.builder.get_dialect_or_config_option(
+        "namedtuple_as_dict", False
+    )
     serialize_option = get_overridden_serialization_method(spec)
     if serialize_option is not None:
         if serialize_option == "as_dict":
@@ -614,11 +662,12 @@ def pack_typed_dict(spec: ValueSpec) -> Expression:
         f"__pack_typed_dict_{spec.builder.cls.__name__}_"
         f"{spec.field_ctx.name}__{random_hex()}"
     )
+    method_args = "self, value" if spec.builder.is_nailed else "value"
     default_kwargs = spec.builder.get_pack_method_default_flag_values()
     if default_kwargs:
-        lines.append(f"def {method_name}(self, value, {default_kwargs}):")
+        lines.append(f"def {method_name}({method_args}, {default_kwargs}):")
     else:
-        lines.append(f"def {method_name}(self, value):")
+        lines.append(f"def {method_name}({method_args}):")
     with lines.indent():
         lines.append("d = {}")
         for key in sorted(required_keys, key=all_keys.index):
@@ -644,7 +693,9 @@ def pack_typed_dict(spec: ValueSpec) -> Expression:
                 )
                 lines.append(f"d['{key}'] = {packer}")
         lines.append("return d")
-    lines.append(f"setattr(cls, '{method_name}', {method_name})")
+    lines.append(
+        f"setattr({spec.cls_attrs_name}, '{method_name}', {method_name})"
+    )
     if spec.builder.get_config().debug:
         print(f"{type_name(spec.builder.cls)}:")
         print(lines.as_text())
@@ -652,7 +703,7 @@ def pack_typed_dict(spec: ValueSpec) -> Expression:
     method_args = ", ".join(
         filter(None, (spec.expression, spec.builder.get_pack_method_flags()))
     )
-    return f"self.{method_name}({method_args})"
+    return f"{spec.self_attrs_name}.{method_name}({method_args})"
 
 
 @register
diff --git a/mashumaro/core/meta/types/unpack.py b/mashumaro/core/meta/types/unpack.py
index c12e1406..575a536d 100644
--- a/mashumaro/core/meta/types/unpack.py
+++ b/mashumaro/core/meta/types/unpack.py
@@ -8,7 +8,7 @@
 import types
 import typing
 import uuid
-from abc import ABC, abstractmethod
+from abc import ABC
 from base64 import decodebytes
 from contextlib import suppress
 from dataclasses import is_dataclass
@@ -59,6 +59,8 @@
     type_name,
 )
 from mashumaro.core.meta.types.common import (
+    AbstractMethodBuilder,
+    AttrsHolder,
     Expression,
     ExpressionWrapper,
     NoneType,
@@ -90,11 +92,11 @@
 
 try:
     import ciso8601
-except ImportError:  # pragma no cover
+except ImportError:  # pragma: no cover
     ciso8601: Optional[types.ModuleType] = None  # type: ignore
 try:
     import pendulum
-except ImportError:  # pragma no cover
+except ImportError:  # pragma: no cover
     pendulum: Optional[types.ModuleType] = None  # type: ignore
 
 
@@ -105,11 +107,7 @@
 register = UnpackerRegistry.register
 
 
-class AbstractUnpackerBuilder(ABC):
-    @abstractmethod
-    def get_method_prefix(self) -> str:  # pragma: no cover
-        raise NotImplementedError
-
+class AbstractUnpackerBuilder(AbstractMethodBuilder, ABC):
     def _generate_method_name(self, spec: ValueSpec) -> str:
         prefix = self.get_method_prefix()
         if prefix:
@@ -126,7 +124,8 @@ def _generate_method_name(self, spec: ValueSpec) -> str:
     def _add_definition(self, spec: ValueSpec, lines: CodeLines) -> str:
         method_name = self._generate_method_name(spec)
         method_args = self._generate_method_args(spec)
-        lines.append("@classmethod")
+        if spec.builder.is_nailed:
+            lines.append("@classmethod")
         lines.append(f"def {method_name}({method_args}):")
         return method_name
 
@@ -140,26 +139,15 @@ def _generate_method_args(self, spec: ValueSpec) -> str:
             extra_args_str = f", {', '.join(extra_args)}"
         else:
             extra_args_str = ""
+        if spec.builder.is_nailed:
+            first_args = "cls, value"
+        else:
+            first_args = "value"
         if default_kwargs:
-            return f"cls, value{extra_args_str}, {default_kwargs}"
+            return f"{first_args}{extra_args_str}, {default_kwargs}"
         else:  # pragma: no cover
             # we shouldn't be here because there will be default_kwargs
-            return f"cls, value{extra_args_str}"
-
-    @abstractmethod
-    def _add_body(
-        self, spec: ValueSpec, lines: CodeLines
-    ) -> None:  # pragma: no cover
-        raise NotImplementedError
-
-    def _add_setattr(self, method_name: str, lines: CodeLines) -> None:
-        lines.append(f"setattr(cls, '{method_name}', {method_name})")
-
-    def _compile(self, spec: ValueSpec, lines: CodeLines) -> None:
-        if spec.builder.get_config().debug:
-            print(f"{type_name(spec.builder.cls)}:")
-            print(lines.as_text())
-        exec(lines.as_text(), spec.builder.globals, spec.builder.__dict__)
+            return f"{first_args}{extra_args_str}"
 
     def _get_call_expr(self, spec: ValueSpec, method_name: str) -> str:
         method_args = ", ".join(
@@ -167,20 +155,7 @@ def _get_call_expr(self, spec: ValueSpec, method_name: str) -> str:
                 None, (spec.expression, spec.builder.get_unpack_method_flags())
             )
         )
-        return f"cls.{method_name}({method_args})"
-
-    def _before_build(self, spec: ValueSpec) -> None:
-        pass
-
-    def build(self, spec: ValueSpec) -> str:
-        self._before_build(spec)
-        lines = CodeLines()
-        method_name = self._add_definition(spec, lines)
-        with lines.indent():
-            self._add_body(spec, lines)
-        self._add_setattr(method_name, lines)
-        self._compile(spec, lines)
-        return self._get_call_expr(spec, method_name)
+        return f"{spec.cls_attrs_name}.{method_name}({method_args})"
 
 
 class UnionUnpackerBuilder(AbstractUnpackerBuilder):
@@ -204,10 +179,13 @@ def _add_body(self, spec: ValueSpec, lines: CodeLines) -> None:
                 spec.field_ctx.name
             ),
         )
-        lines.append(
-            f"raise InvalidFieldValue('{spec.field_ctx.name}',{field_type},"
-            f"value,cls)"
-        )
+        if spec.builder.is_nailed:
+            lines.append(
+                f"raise InvalidFieldValue("
+                f"'{spec.field_ctx.name}',{field_type},value,cls)"
+            )
+        else:
+            lines.append("raise ValueError(value)")
 
 
 class TypeVarUnpackerBuilder(UnionUnpackerBuilder):
@@ -274,7 +252,10 @@ def _get_variants_attr(self, spec: ValueSpec) -> str:
 
     def _get_variants_map(self, spec: ValueSpec) -> str:
         variants_attr = self._get_variants_attr(spec)
-        return f"{type_name(spec.builder.cls)}.{variants_attr}"
+        if spec.builder.is_nailed:
+            return f"{type_name(spec.builder.cls)}.{variants_attr}"
+        else:
+            return f"{spec.cls_attrs_name}.{variants_attr}"
 
     def _get_variant_names(self, spec: ValueSpec) -> List[str]:
         base_variants = self.base_variants or (spec.origin_type,)
@@ -300,7 +281,7 @@ def _get_variant_names_iterable(self, spec: ValueSpec) -> str:
 
     @staticmethod
     def _get_variants_attr_holder(spec: ValueSpec) -> Type:
-        return spec.builder.cls
+        return spec.attrs
 
     @staticmethod
     def _get_variant_method_call(method_name: str, spec: ValueSpec) -> str:
@@ -356,7 +337,13 @@ def _add_body(self, spec: ValueSpec, lines: CodeLines) -> None:
                     f" from None"
                 )
             with lines.indent("try:"):
-                lines.append(f"return {chosen_cls}.{variant_method_call}")
+                if spec.builder.is_nailed:
+                    lines.append(f"return {chosen_cls}.{variant_method_call}")
+                else:
+                    lines.append(
+                        f"return {spec.attrs_registry_name}"
+                        f"[{chosen_cls}].{variant_method_call}"
+                    )
             with lines.indent("except (KeyError, AttributeError):"):
                 lines.append(f"variants_map = {variants_map}")
                 with lines.indent(f"for variant in {variants}:"):
@@ -366,29 +353,21 @@ def _add_body(self, spec: ValueSpec, lines: CodeLines) -> None:
                         )
                     with lines.indent("except KeyError:"):
                         lines.append("continue")
-                    spec.builder.ensure_object_imported(
-                        get_class_that_defines_method
-                    )
-                    lines.append(
-                        f"if get_class_that_defines_method("
-                        f"'{variant_method_name}',variant) != variant:"
+                    self._add_build_variant_unpacker(
+                        spec, lines, variant_method_name, variant_method_call
                     )
-                    with lines.indent():
-                        spec.builder.ensure_object_imported(
-                            spec.builder.__class__
+                with lines.indent("try:"):
+                    if spec.builder.is_nailed:
+                        lines.append(
+                            f"return variants_map[discriminator]"
+                            f".{variant_method_call}"
                         )
+                    else:
                         lines.append(
-                            f"CodeBuilder(variant, "
-                            f"dialect=_dialect, "
-                            f"format_name={repr(spec.builder.format_name)}, "
-                            f"default_dialect=_default_dialect)"
-                            f".add_unpack_method()"
+                            f"return {spec.attrs_registry_name}["
+                            f"variants_map[discriminator]]"
+                            f".{variant_method_call}"
                         )
-                with lines.indent("try:"):
-                    lines.append(
-                        f"return variants_map[discriminator]"
-                        f".{variant_method_call}"
-                    )
                 with lines.indent("except KeyError:"):
                     lines.append(
                         f"raise SuitableVariantNotFoundError("
@@ -398,31 +377,21 @@ def _add_body(self, spec: ValueSpec, lines: CodeLines) -> None:
         else:
             with lines.indent(f"for variant in {variants}:"):
                 with lines.indent("try:"):
-                    lines.append(f"return variant.{variant_method_call}")
-                with lines.indent("except AttributeError:"):
-                    spec.builder.ensure_object_imported(
-                        get_class_that_defines_method
-                    )
-                    lines.append(
-                        f"if get_class_that_defines_method("
-                        f"'{variant_method_name}',variant) != variant:"
-                    )
-                    with lines.indent():
-                        spec.builder.ensure_object_imported(
-                            spec.builder.__class__
-                        )
+                    if spec.builder.is_nailed:
+                        lines.append(f"return variant.{variant_method_call}")
+                    else:
                         lines.append(
-                            f"CodeBuilder(variant, "
-                            f"dialect=_dialect, "
-                            f"format_name={repr(spec.builder.format_name)}, "
-                            f"default_dialect=_default_dialect)"
-                            f".add_unpack_method()"
+                            f"return {spec.attrs_registry_name}"
+                            f"[variant].{variant_method_call}"
                         )
-                        with lines.indent("try:"):
-                            lines.append(
-                                f"return variant.{variant_method_call}"
-                            )
-                        lines.append("except Exception: pass")
+                if spec.builder.is_nailed:
+                    exc_to_catch = "AttributeError"
+                else:
+                    exc_to_catch = "(KeyError, AttributeError)"
+                with lines.indent(f"except {exc_to_catch}:"):
+                    self._add_build_variant_unpacker(
+                        spec, lines, variant_method_name, variant_method_call
+                    )
                 lines.append("except Exception: pass")
             lines.append(
                 f"raise SuitableVariantNotFoundError({variants_type_expr}) "
@@ -441,7 +410,52 @@ def _get_call_expr(self, spec: ValueSpec, method_name: str) -> str:
                 ),
             )
         )
-        return f"cls.{method_name}({method_args})"
+        return f"{spec.cls_attrs_name}.{method_name}({method_args})"
+
+    def _add_build_variant_unpacker(
+        self,
+        spec: ValueSpec,
+        lines: CodeLines,
+        variant_method_name: str,
+        variant_method_call: str,
+    ) -> None:
+        if spec.builder.is_nailed:
+            spec.builder.ensure_object_imported(get_class_that_defines_method)
+            lines.append(
+                f"if get_class_that_defines_method("
+                f"'{variant_method_name}',variant) != variant:"
+            )
+            with lines.indent():
+                spec.builder.ensure_object_imported(spec.builder.__class__)
+                lines.append(
+                    f"CodeBuilder(variant, "
+                    f"dialect=_dialect, "
+                    f"format_name={repr(spec.builder.format_name)}, "
+                    f"default_dialect=_default_dialect)"
+                    f".add_unpack_method()"
+                )
+                if not self.discriminator.field:
+                    with lines.indent("try:"):
+                        lines.append(f"return variant.{variant_method_call}")
+                    lines.append("except Exception: pass")
+        else:
+            spec.builder.ensure_object_imported(AttrsHolder)
+            attrs = f"attrs_{random_hex()}"
+            lines.append(f"{attrs} = AttrsHolder('{attrs}')")
+            lines.append(f"{spec.attrs_registry_name}[variant] = {attrs}")
+            lines.append(
+                f"CodeBuilder(variant, "
+                f"dialect=_dialect, "
+                f"format_name={repr(spec.builder.format_name)}, "
+                f"default_dialect=_default_dialect,"
+                f"attrs={attrs},"
+                f"attrs_registry={spec.attrs_registry_name})"
+                f".add_unpack_method()"
+            )
+            if not self.discriminator.field:
+                with lines.indent("try:"):
+                    lines.append(f"return {attrs}.{variant_method_call}")
+                lines.append("except Exception: pass")
 
 
 class SubtypeUnpackerBuilder(DiscriminatedUnionUnpackerBuilder):
@@ -451,10 +465,6 @@ def _get_variants_attr(self, spec: ValueSpec) -> str:
             self._variants_attr = "__mashumaro_subtype_variants__"
         return self._variants_attr
 
-    def _get_variants_map(self, spec: ValueSpec) -> str:
-        variants_attr = self._get_variants_attr(spec)
-        return f"{type_name(spec.origin_type)}.{variants_attr}"
-
 
 def _unpack_with_annotated_serialization_strategy(
     spec: ValueSpec,
@@ -476,9 +486,9 @@ def _unpack_with_annotated_serialization_strategy(
         resolve_type_params(strategy_type, get_args(spec.type))[strategy_type],
     )
     overridden_fn = f"__{spec.field_ctx.name}_deserialize_{random_hex()}"
-    setattr(spec.builder.cls, overridden_fn, strategy.deserialize)
+    setattr(spec.attrs, overridden_fn, strategy.deserialize)
     unpacker = UnpackerRegistry.get(spec.copy(type=value_type))
-    return f"cls.{overridden_fn}({unpacker})"
+    return f"{spec.cls_attrs_name}.{overridden_fn}({unpacker})"
 
 
 def get_overridden_deserialization_method(
@@ -522,8 +532,8 @@ def unpack_type_with_overridden_deserialization(
         return deserialization_method.expression
     elif callable(deserialization_method):
         overridden_fn = f"__{spec.field_ctx.name}_deserialize_{random_hex()}"
-        setattr(spec.builder.cls, overridden_fn, deserialization_method)
-        return f"cls.{overridden_fn}({spec.expression})"
+        setattr(spec.attrs, overridden_fn, deserialization_method)
+        return f"{spec.cls_attrs_name}.{overridden_fn}({spec.expression})"
 
 
 def _unpack_annotated_serializable_type(
@@ -596,9 +606,10 @@ def unpack_dataclass(spec: ValueSpec) -> Optional[Expression]:
         method_name = spec.builder.get_unpack_method_name(
             type_args, spec.builder.format_name
         )
+        method_loc = spec.origin_type if spec.builder.is_nailed else spec.attrs
         if get_class_that_defines_method(
-            method_name, spec.origin_type
-        ) != spec.origin_type and (
+            method_name, method_loc
+        ) != method_loc and (
             spec.origin_type != spec.builder.cls
             or spec.builder.get_unpack_method_name(
                 type_args=type_args,
@@ -613,6 +624,10 @@ def unpack_dataclass(spec: ValueSpec) -> Optional[Expression]:
                 dialect=spec.builder.dialect,
                 format_name=spec.builder.format_name,
                 default_dialect=spec.builder.default_dialect,
+                attrs=method_loc,
+                attrs_registry=(
+                    spec.attrs_registry if not spec.builder.is_nailed else None
+                ),
             )
             builder.add_unpack_method()
         method_args = ", ".join(
@@ -625,8 +640,16 @@ def unpack_dataclass(spec: ValueSpec) -> Optional[Expression]:
             )
         )
         cls_alias = clean_id(type_name(spec.origin_type))
-        spec.builder.ensure_object_imported(spec.origin_type, cls_alias)
-        return f"{cls_alias}.{method_name}({method_args})"
+        if spec.builder.is_nailed:
+            spec.builder.ensure_object_imported(spec.origin_type, cls_alias)
+            return f"{cls_alias}.{method_name}({method_args})"
+        else:
+            method_name_alias = f"{cls_alias}_{method_name}"
+            spec.builder.ensure_object_imported(
+                getattr(spec.attrs, method_name),
+                method_name_alias,
+            )
+            return f"{method_name_alias}({method_args})"
 
 
 @register
@@ -689,9 +712,12 @@ def unpack_special_typing_primitive(spec: ValueSpec) -> Optional[Expression]:
             method_name = spec.builder.get_unpack_method_name(
                 format_name=spec.builder.format_name
             )
+            method_loc = (
+                spec.builder.cls if spec.builder.is_nailed else spec.attrs
+            )
             if (
-                get_class_that_defines_method(method_name, spec.builder.cls)
-                != spec.builder.cls
+                get_class_that_defines_method(method_name, method_loc)
+                != method_loc
                 # not hasattr(spec.builder.cls, method_name)
                 and spec.builder.get_unpack_method_name(
                     format_name=spec.builder.format_name,
@@ -704,6 +730,12 @@ def unpack_special_typing_primitive(spec: ValueSpec) -> Optional[Expression]:
                     dialect=spec.builder.dialect,
                     format_name=spec.builder.format_name,
                     default_dialect=spec.builder.default_dialect,
+                    attrs=method_loc,
+                    attrs_registry=(
+                        spec.attrs_registry
+                        if not spec.builder.is_nailed
+                        else None
+                    ),
                 )
                 builder.add_unpack_method()
             method_args = ", ".join(
@@ -715,10 +747,14 @@ def unpack_special_typing_primitive(spec: ValueSpec) -> Optional[Expression]:
                     ),
                 )
             )
-            spec.builder.add_type_modules(spec.builder.cls)
-            return (
-                f"{type_name(spec.builder.cls)}.{method_name}({method_args})"
-            )
+            if spec.builder.is_nailed:
+                spec.builder.add_type_modules(spec.builder.cls)
+                return (
+                    f"{type_name(spec.builder.cls)}.{method_name}"
+                    f"({method_args})"
+                )
+            else:
+                return f"_cls.{method_name}({method_args})"
         elif is_required(spec.type) or is_not_required(spec.type):
             return UnpackerRegistry.get(spec.copy(type=get_args(spec.type)[0]))
         elif is_unpack(spec.type):
@@ -763,7 +799,7 @@ def unpack_date_objects(spec: ValueSpec) -> Optional[Expression]:
                 else:
                     raise ThirdPartyModuleNotFoundError(
                         "ciso8601", spec.field_ctx.name, spec.builder.cls
-                    )  # pragma no cover
+                    )  # pragma: no cover
             elif deserialize_option == "pendulum":
                 if pendulum:
                     spec.builder.ensure_module_imported(pendulum)
@@ -771,7 +807,7 @@ def unpack_date_objects(spec: ValueSpec) -> Optional[Expression]:
                 else:
                     raise ThirdPartyModuleNotFoundError(
                         "pendulum", spec.field_ctx.name, spec.builder.cls
-                    )  # pragma no cover
+                    )  # pragma: no cover
             else:
                 raise UnsupportedDeserializationEngine(
                     spec.field_ctx.name,
@@ -918,7 +954,9 @@ def unpack_named_tuple(spec: ValueSpec) -> Expression:
     fields = getattr(spec.type, "_fields", ())
     defaults = getattr(spec.type, "_field_defaults", {})
     unpackers = []
-    as_dict = spec.builder.get_config().namedtuple_as_dict
+    as_dict = spec.builder.get_dialect_or_config_option(
+        "namedtuple_as_dict", False
+    )
     deserialize_option = get_overridden_deserialization_method(spec)
     if deserialize_option is not None:
         if deserialize_option == "as_dict":
@@ -959,13 +997,17 @@ def unpack_named_tuple(spec: ValueSpec) -> Expression:
         f"__unpack_named_tuple_{spec.builder.cls.__name__}_"
         f"{spec.field_ctx.name}__{random_hex()}"
     )
-    lines.append("@classmethod")
     default_kwargs = spec.builder.get_unpack_method_default_flag_values()
+    if spec.builder.is_nailed:
+        lines.append("@classmethod")
+        method_args = "cls, value"
+    else:
+        method_args = "value"
     if default_kwargs:
-        lines.append(f"def {method_name}(cls, value, {default_kwargs}):")
+        lines.append(f"def {method_name}({method_args}, {default_kwargs}):")
     else:  # pragma: no cover
         # we shouldn't be here because there will be default_kwargs
-        lines.append(f"def {method_name}(cls, value):")
+        lines.append(f"def {method_name}({method_args}):")
     with lines.indent():
         lines.append("fields = []")
         with lines.indent("try:"):
@@ -974,7 +1016,9 @@ def unpack_named_tuple(spec: ValueSpec) -> Expression:
         with lines.indent("except IndexError:"):
             lines.append("pass")
         lines.append(f"return {type_name(spec.type)}(*fields)")
-    lines.append(f"setattr(cls, '{method_name}', {method_name})")
+    lines.append(
+        f"setattr({spec.cls_attrs_name}, '{method_name}', {method_name})"
+    )
     if spec.builder.get_config().debug:
         print(f"{type_name(spec.builder.cls)}:")
         print(lines.as_text())
@@ -982,7 +1026,7 @@ def unpack_named_tuple(spec: ValueSpec) -> Expression:
     method_args = ", ".join(
         filter(None, (spec.expression, spec.builder.get_unpack_method_flags()))
     )
-    return f"cls.{method_name}({method_args})"
+    return f"{spec.cls_attrs_name}.{method_name}({method_args})"
 
 
 def unpack_typed_dict(spec: ValueSpec) -> Expression:
@@ -1002,12 +1046,16 @@ def unpack_typed_dict(spec: ValueSpec) -> Expression:
         f"{spec.field_ctx.name}__{random_hex()}"
     )
     default_kwargs = spec.builder.get_unpack_method_default_flag_values()
-    lines.append("@classmethod")
+    if spec.builder.is_nailed:
+        lines.append("@classmethod")
+        method_args = "cls, value"
+    else:
+        method_args = "value"
     if default_kwargs:
-        lines.append(f"def {method_name}(cls, value, {default_kwargs}):")
+        lines.append(f"def {method_name}({method_args}, {default_kwargs}):")
     else:  # pragma: no cover
         # we shouldn't be here because there will be default_kwargs
-        lines.append(f"def {method_name}(cls, value):")
+        lines.append(f"def {method_name}({method_args}):")
     with lines.indent():
         lines.append("d = {}")
         for key in sorted(required_keys, key=all_keys.index):
@@ -1033,7 +1081,9 @@ def unpack_typed_dict(spec: ValueSpec) -> Expression:
                 )
                 lines.append(f"d['{key}'] = {unpacker}")
         lines.append("return d")
-    lines.append(f"setattr(cls, '{method_name}', {method_name})")
+    lines.append(
+        f"setattr({spec.cls_attrs_name}, '{method_name}', {method_name})"
+    )
     if spec.builder.get_config().debug:
         print(f"{type_name(spec.builder.cls)}:")
         print(lines.as_text())
@@ -1041,7 +1091,7 @@ def unpack_typed_dict(spec: ValueSpec) -> Expression:
     method_args = ", ".join(
         filter(None, (spec.expression, spec.builder.get_unpack_method_flags()))
     )
-    return f"cls.{method_name}({method_args})"
+    return f"{spec.cls_attrs_name}.{method_name}({method_args})"
 
 
 @register
diff --git a/mashumaro/dialect.py b/mashumaro/dialect.py
index d1e09b97..417ae9f6 100644
--- a/mashumaro/dialect.py
+++ b/mashumaro/dialect.py
@@ -1,4 +1,5 @@
-from typing import Any, Callable, Dict, Sequence, Union
+from types import new_class
+from typing import Any, Callable, Dict, Sequence, Type, Union, cast
 
 from typing_extensions import Literal
 
@@ -15,6 +16,46 @@
 
 class Dialect:
     serialization_strategy: Dict[Any, SerializationStrategyValueType] = {}
+    serialize_by_alias: Union[
+        bool, Literal[Sentinel.MISSING]
+    ] = Sentinel.MISSING
+    namedtuple_as_dict: Union[
+        bool, Literal[Sentinel.MISSING]
+    ] = Sentinel.MISSING
     omit_none: Union[bool, Literal[Sentinel.MISSING]] = Sentinel.MISSING
     omit_default: Union[bool, Literal[Sentinel.MISSING]] = Sentinel.MISSING
-    no_copy_collections: Sequence[Any] = tuple()
+    no_copy_collections: Union[
+        Sequence[Any], Literal[Sentinel.MISSING]
+    ] = Sentinel.MISSING
+
+    @classmethod
+    def merge(cls, other: Type["Dialect"]) -> Type["Dialect"]:
+        serialization_strategy: Dict[Any, SerializationStrategyValueType] = {}
+        for key, value in cls.serialization_strategy.items():
+            if isinstance(value, SerializationStrategy):
+                serialization_strategy[key] = value
+            else:
+                serialization_strategy[key] = value.copy()
+        for key, value in other.serialization_strategy.items():
+            if isinstance(value, SerializationStrategy):
+                serialization_strategy[key] = value
+            elif isinstance(
+                serialization_strategy.get(key), SerializationStrategy
+            ):
+                serialization_strategy[key] = value
+            else:
+                (
+                    serialization_strategy.setdefault(
+                        key, {}
+                    ).update(  # type: ignore
+                        value
+                    )
+                )
+        new_dialect = cast(Type[Dialect], new_class("Dialect", (Dialect,)))
+        new_dialect.serialization_strategy = serialization_strategy
+        for key in ("omit_none", "omit_default", "no_copy_collections"):
+            if (others_value := getattr(other, key)) is not Sentinel.MISSING:
+                setattr(new_dialect, key, others_value)
+            else:
+                setattr(new_dialect, key, getattr(cls, key))
+        return new_dialect
diff --git a/mashumaro/jsonschema/builder.py b/mashumaro/jsonschema/builder.py
index f491a240..9367461f 100644
--- a/mashumaro/jsonschema/builder.py
+++ b/mashumaro/jsonschema/builder.py
@@ -9,7 +9,7 @@
     from mashumaro.mixins.orjson import (
         DataClassORJSONMixin as DataClassJSONMixin,
     )
-except ImportError:  # pragma no cover
+except ImportError:  # pragma: no cover
     from mashumaro.mixins.json import DataClassJSONMixin  # type: ignore
 
 
diff --git a/mashumaro/jsonschema/models.py b/mashumaro/jsonschema/models.py
index dbf43e18..7b85224e 100644
--- a/mashumaro/jsonschema/models.py
+++ b/mashumaro/jsonschema/models.py
@@ -14,7 +14,7 @@
     from mashumaro.mixins.orjson import (
         DataClassORJSONMixin as DataClassJSONMixin,
     )
-except ImportError:  # pragma no cover
+except ImportError:  # pragma: no cover
     from mashumaro.mixins.json import DataClassJSONMixin  # type: ignore
 
 
diff --git a/mashumaro/jsonschema/schema.py b/mashumaro/jsonschema/schema.py
index becc9908..ee9631f1 100644
--- a/mashumaro/jsonschema/schema.py
+++ b/mashumaro/jsonschema/schema.py
@@ -91,7 +91,7 @@
     from mashumaro.mixins.orjson import (
         DataClassORJSONMixin as DataClassJSONMixin,
     )
-except ImportError:  # pragma no cover
+except ImportError:  # pragma: no cover
     from mashumaro.mixins.json import DataClassJSONMixin  # type: ignore
 
 
@@ -212,6 +212,16 @@ def get_owner_config(self) -> Type[BaseConfig]:
         else:
             return BaseConfig
 
+    def get_owner_dialect_or_config_option(
+        self, option: str, default: Any
+    ) -> Any:
+        if self.__owner_builder:
+            return self.__owner_builder.get_dialect_or_config_option(
+                option, default
+            )
+        else:
+            return default
+
     def get_self_config(self) -> Type[BaseConfig]:
         if self.__self_builder:
             return self.__self_builder.get_config()
@@ -606,7 +616,9 @@ def on_named_tuple(instance: Instance, ctx: Context) -> JSONSchema:
     }
     fields = getattr(instance.type, "_fields", ())
     defaults = getattr(instance.type, "_field_defaults", {})
-    as_dict = instance.get_owner_config().namedtuple_as_dict
+    as_dict = instance.get_owner_dialect_or_config_option(
+        "namedtuple_as_dict", False
+    )
     serialize_option = instance.get_overridden_serialization_method()
     if serialize_option == "as_dict":
         as_dict = True
diff --git a/mashumaro/mixins/dict.py b/mashumaro/mixins/dict.py
index 05311f1b..e18fbbc2 100644
--- a/mashumaro/mixins/dict.py
+++ b/mashumaro/mixins/dict.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict, Mapping, Type, TypeVar
+from typing import Any, Dict, Mapping, Type, TypeVar, final
 
 from mashumaro.core.meta.mixin import (
     compile_mixin_packer,
@@ -24,6 +24,7 @@ def __init_subclass__(cls: Type[T], **kwargs: Any):
                 compile_mixin_unpacker(cls, **builder_params["unpacker"])
                 compile_mixin_packer(cls, **builder_params["packer"])
 
+    @final
     def to_dict(
         self: T,
         # *
@@ -36,6 +37,7 @@ def to_dict(
         ...
 
     @classmethod
+    @final
     def from_dict(
         cls: Type[T],
         d: Mapping,
diff --git a/mashumaro/mixins/msgpack.py b/mashumaro/mixins/msgpack.py
index 18e1f4e4..ceae3f61 100644
--- a/mashumaro/mixins/msgpack.py
+++ b/mashumaro/mixins/msgpack.py
@@ -1,4 +1,4 @@
-from typing import Any, Callable, Dict, Type, TypeVar
+from typing import Any, Callable, Dict, Type, TypeVar, final
 
 import msgpack
 
@@ -49,6 +49,7 @@ class DataClassMessagePackMixin(DataClassDictMixin):
         },
     }
 
+    @final
     def to_msgpack(
         self: T,
         encoder: Encoder = default_encoder,
@@ -57,6 +58,7 @@ def to_msgpack(
         ...
 
     @classmethod
+    @final
     def from_msgpack(
         cls: Type[T],
         data: EncodedData,
diff --git a/mashumaro/mixins/orjson.py b/mashumaro/mixins/orjson.py
index 87c6f1a7..2e9887ed 100644
--- a/mashumaro/mixins/orjson.py
+++ b/mashumaro/mixins/orjson.py
@@ -1,5 +1,5 @@
 from datetime import date, datetime, time
-from typing import Any, Callable, Dict, Type, TypeVar, Union
+from typing import Any, Callable, Dict, Type, TypeVar, Union, final
 from uuid import UUID
 
 import orjson
@@ -46,6 +46,7 @@ class DataClassORJSONMixin(DataClassDictMixin):
         },
     }
 
+    @final
     def to_jsonb(
         self: T,
         encoder: Encoder = orjson.dumps,
@@ -59,6 +60,7 @@ def to_json(self: T, **kwargs: Any) -> str:
         return self.to_jsonb(**kwargs).decode()
 
     @classmethod
+    @final
     def from_json(
         cls: Type[T],
         data: EncodedData,
diff --git a/mashumaro/mixins/orjson.pyi b/mashumaro/mixins/orjson.pyi
index ebcf3427..3f9121c0 100644
--- a/mashumaro/mixins/orjson.pyi
+++ b/mashumaro/mixins/orjson.pyi
@@ -1,4 +1,4 @@
-from typing import Any, Callable, Dict, Type, TypeVar, Union
+from typing import Any, Callable, Dict, Type, TypeVar, Union, final
 
 import orjson
 
@@ -16,6 +16,7 @@ class OrjsonDialect(Dialect):
 
 class DataClassORJSONMixin(DataClassDictMixin):
     __slots__ = ()
+    @final
     def to_jsonb(
         self: T,
         encoder: Encoder = orjson.dumps,
@@ -31,6 +32,7 @@ class DataClassORJSONMixin(DataClassDictMixin):
         **to_dict_kwargs: Any,
     ) -> bytes: ...
     @classmethod
+    @final
     def from_json(
         cls: Type[T],
         data: EncodedData,
diff --git a/mashumaro/mixins/toml.py b/mashumaro/mixins/toml.py
index 92091588..132ab485 100644
--- a/mashumaro/mixins/toml.py
+++ b/mashumaro/mixins/toml.py
@@ -1,5 +1,5 @@
 from datetime import date, datetime, time
-from typing import Any, Callable, Dict, Type, TypeVar
+from typing import Any, Callable, Dict, Type, TypeVar, final
 
 import tomli_w
 
@@ -46,6 +46,7 @@ class DataClassTOMLMixin(DataClassDictMixin):
         },
     }
 
+    @final
     def to_toml(
         self: T,
         encoder: Encoder = tomli_w.dumps,
@@ -54,6 +55,7 @@ def to_toml(
         ...
 
     @classmethod
+    @final
     def from_toml(
         cls: Type[T],
         data: EncodedData,
diff --git a/pyproject.toml b/pyproject.toml
index 73668de6..a6ea37a9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,6 +8,7 @@ module = [
     'mashumaro.mixins.dict',
     'mashumaro.mixins.msgpack',
     'mashumaro.mixins.toml',
+    'mashumaro.codecs.*',
 ]
 disable_error_code = 'empty-body'
 
@@ -29,3 +30,12 @@ ensure_newline_before_comments = true
 [tool.black]
 line-length = 79
 target-version = ['py38', 'py39', 'py310', 'py311']
+
+[tool.ruff]
+line-length = 79
+
+[tool.coverage.run]
+omit = ["setup.py"]
+
+[tool.coverage.report]
+exclude_lines = ["pragma: no cover", "@overload", "@abstractmethod"]


Test patch:
diff --git a/tests/entities.py b/tests/entities.py
index 7ac2c05b..83ec603d 100644
--- a/tests/entities.py
+++ b/tests/entities.py
@@ -7,7 +7,7 @@
 
 try:
     from enum import StrEnum
-except ImportError:  # pragma no cover
+except ImportError:  # pragma: no cover
 
     class StrEnum(str, Enum):
         pass
diff --git a/tests/test_codecs/__init__.py b/tests/test_codecs/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/test_codecs/test_basic_codec.py b/tests/test_codecs/test_basic_codec.py
new file mode 100644
index 00000000..cc0de5dc
--- /dev/null
+++ b/tests/test_codecs/test_basic_codec.py
@@ -0,0 +1,247 @@
+from dataclasses import dataclass
+from datetime import date, datetime
+from typing import Generic, List, Optional, TypeVar, Union
+
+import pytest
+from typing_extensions import Literal
+
+from mashumaro.codecs import BasicDecoder, BasicEncoder
+from mashumaro.codecs.basic import decode, encode
+from mashumaro.dialect import Dialect
+from tests.entities import (
+    DataClassWithoutMixin,
+    GenericNamedTuple,
+    GenericTypedDict,
+    MyGenericDataClass,
+)
+
+T = TypeVar("T")
+
+
+@dataclass
+class Foo:
+    foo: str
+
+
+@dataclass
+class Bar:
+    bar: str
+
+
+class MyDialect(Dialect):
+    serialization_strategy = {
+        date: {
+            "serialize": date.toordinal,
+            "deserialize": date.fromordinal,
+        },
+    }
+
+
+@dataclass
+class GenericDataClass(Generic[T]):
+    x: T
+    y: List[T]
+
+
+@pytest.mark.parametrize(
+    ("shape_type", "encoded", "decoded"),
+    [
+        [
+            List[date],
+            ["2023-09-22", "2023-09-23"],
+            [
+                date(2023, 9, 22),
+                date(2023, 9, 23),
+            ],
+        ],
+        [DataClassWithoutMixin, {"i": 42}, DataClassWithoutMixin(42)],
+        [
+            List[DataClassWithoutMixin],
+            [{"i": 42}],
+            [DataClassWithoutMixin(42)],
+        ],
+        [
+            GenericDataClass,
+            {"x": "2023-09-23", "y": ["2023-09-23"]},
+            GenericDataClass("2023-09-23", ["2023-09-23"]),
+        ],
+        [
+            GenericDataClass[date],
+            {"x": "2023-09-23", "y": ["2023-09-23"]},
+            GenericDataClass(date(2023, 9, 23), [date(2023, 9, 23)]),
+        ],
+        [
+            List[Union[int, date]],
+            ["42", "2023-09-23"],
+            [42, date(2023, 9, 23)],
+        ],
+        [Optional[date], "2023-09-23", date(2023, 9, 23)],
+        [Optional[date], None, None],
+    ],
+)
+def test_decode(shape_type, encoded, decoded):
+    assert decode(encoded, shape_type) == decoded
+
+
+@pytest.mark.parametrize(
+    ("shape_type", "encoded", "decoded"),
+    [
+        [
+            List[date],
+            ["2023-09-22", "2023-09-23"],
+            [
+                date(2023, 9, 22),
+                date(2023, 9, 23),
+            ],
+        ],
+        [DataClassWithoutMixin, {"i": 42}, DataClassWithoutMixin(42)],
+        [
+            List[DataClassWithoutMixin],
+            [{"i": 42}],
+            [DataClassWithoutMixin(42)],
+        ],
+        [
+            GenericDataClass,
+            {"x": date(2023, 9, 23), "y": [date(2023, 9, 23)]},
+            GenericDataClass(date(2023, 9, 23), [date(2023, 9, 23)]),
+        ],
+        [
+            GenericDataClass[date],
+            {"x": "2023-09-23", "y": ["2023-09-23"]},
+            GenericDataClass(date(2023, 9, 23), [date(2023, 9, 23)]),
+        ],
+        [List[Union[int, date]], [42, "2023-09-23"], [42, date(2023, 9, 23)]],
+        [Optional[date], "2023-09-23", date(2023, 9, 23)],
+        [Optional[date], None, None],
+    ],
+)
+def test_encode(shape_type, encoded, decoded):
+    assert encode(decoded, shape_type) == encoded
+
+
+def test_decoder_with_default_dialect():
+    decoder = BasicDecoder(List[date], default_dialect=MyDialect)
+    assert decoder.decode([738785, 738786]) == [
+        date(2023, 9, 22),
+        date(2023, 9, 23),
+    ]
+
+
+def test_encoder_with_default_dialect():
+    encoder = BasicEncoder(List[date], default_dialect=MyDialect)
+    assert encoder.encode([date(2023, 9, 22), date(2023, 9, 23)]) == [
+        738785,
+        738786,
+    ]
+
+
+def test_pre_decoder_func():
+    decoder = BasicDecoder(List[date], pre_decoder_func=lambda v: v.split(","))
+    assert decoder.decode("2023-09-22,2023-09-23") == [
+        date(2023, 9, 22),
+        date(2023, 9, 23),
+    ]
+
+
+def test_post_encoder_func():
+    encoder = BasicEncoder(List[date], post_encoder_func=lambda v: ",".join(v))
+    assert (
+        encoder.encode(
+            [
+                date(2023, 9, 22),
+                date(2023, 9, 23),
+            ]
+        )
+        == "2023-09-22,2023-09-23"
+    )
+
+
+@pytest.mark.parametrize(
+    ("shape_type", "invalid_value"),
+    [[Union[date, datetime], "foo"], [Literal["foo"], "bar"]],
+)
+def test_value_error_on_decode(shape_type, invalid_value):
+    decoder = BasicDecoder(shape_type)
+    with pytest.raises(ValueError) as e:
+        decoder.decode(invalid_value)
+    assert type(e.value) is ValueError
+
+
+@pytest.mark.parametrize(
+    ("shape_type", "invalid_value"),
+    [[Union[date, datetime], "foo"], [Literal["foo"], "bar"]],
+)
+def test_value_error_on_encode(shape_type, invalid_value):
+    encoder = BasicEncoder(shape_type)
+    with pytest.raises(ValueError) as e:
+        encoder.encode(invalid_value)
+    assert type(e.value) is ValueError
+
+
+def test_with_fields_with_generated_methods():
+    @dataclass
+    class MyClass:
+        td1: GenericTypedDict[str]
+        td2: GenericTypedDict[date]
+        nt1: GenericNamedTuple[str]
+        nt2: GenericNamedTuple[date]
+        u1: List[Union[int, str]]
+        u2: List[Union[int, date]]
+        l1: Literal["l1"]
+        l2: Literal["l2"]
+
+    decoder = BasicDecoder(MyClass)
+    encoder = BasicEncoder(MyClass)
+    data = {
+        "td1": {"x": "2023-11-15", "y": 1},
+        "td2": {"x": "2023-11-15", "y": 2},
+        "nt1": ["2023-11-15", 3],
+        "nt2": ["2023-11-15", 4],
+        "u1": [5, "2023-11-15"],
+        "u2": [6, "2023-11-15"],
+        "l1": "l1",
+        "l2": "l2",
+    }
+    obj = MyClass(
+        td1={"x": "2023-11-15", "y": 1},
+        td2={"x": date(2023, 11, 15), "y": 2},
+        nt1=GenericNamedTuple("2023-11-15", 3),
+        nt2=GenericNamedTuple(date(2023, 11, 15), 4),
+        u1=[5, "2023-11-15"],
+        u2=[6, date(2023, 11, 15)],
+        l1="l1",
+        l2="l2",
+    )
+    assert decoder.decode(data) == obj
+    assert encoder.encode(obj) == data
+
+
+def test_with_two_dataclass_fields():
+    @dataclass
+    class MyClass:
+        x1: Foo
+        x2: Bar
+
+    decoder = BasicDecoder(MyClass)
+    encoder = BasicEncoder(MyClass)
+    data = {"x1": {"foo": "foo"}, "x2": {"bar": "bar"}}
+    obj = MyClass(x1=Foo("foo"), x2=Bar("bar"))
+    assert decoder.decode(data) == obj
+    assert encoder.encode(obj) == data
+
+
+def test_with_two_generic_dataclass_fields():
+    @dataclass
+    class MyClass:
+        x1: MyGenericDataClass[str]
+        x2: MyGenericDataClass[date]
+
+    decoder = BasicDecoder(MyClass)
+    encoder = BasicEncoder(MyClass)
+    data = {"x1": {"x": "2023-11-15"}, "x2": {"x": "2023-11-15"}}
+    obj = MyClass(
+        x1=MyGenericDataClass("2023-11-15"),
+        x2=MyGenericDataClass(date(2023, 11, 15)),
+    )
+    assert decoder.decode(data) == obj
+    assert encoder.encode(obj) == data
diff --git a/tests/test_codecs/test_json_codec.py b/tests/test_codecs/test_json_codec.py
new file mode 100644
index 00000000..eb620090
--- /dev/null
+++ b/tests/test_codecs/test_json_codec.py
@@ -0,0 +1,79 @@
+import json
+from datetime import date
+from typing import List
+
+from mashumaro.codecs.json import (
+    JSONDecoder,
+    JSONEncoder,
+    json_decode,
+    json_encode,
+)
+from mashumaro.dialect import Dialect
+
+
+class MyDialect(Dialect):
+    serialization_strategy = {
+        date: {
+            "serialize": date.toordinal,
+            "deserialize": date.fromordinal,
+        },
+    }
+
+
+def test_json_decode():
+    assert json_decode('["2023-09-22", "2023-09-23"]', List[date]) == [
+        date(2023, 9, 22),
+        date(2023, 9, 23),
+    ]
+
+
+def test_json_encode():
+    assert (
+        json_encode([date(2023, 9, 22), date(2023, 9, 23)], List[date])
+        == '["2023-09-22", "2023-09-23"]'
+    )
+
+
+def test_decoder_with_default_dialect():
+    decoder = JSONDecoder(List[date], default_dialect=MyDialect)
+    assert decoder.decode("[738785, 738786]") == [
+        date(2023, 9, 22),
+        date(2023, 9, 23),
+    ]
+
+
+def test_encoder_with_default_dialect():
+    encoder = JSONEncoder(List[date], default_dialect=MyDialect)
+    assert (
+        encoder.encode([date(2023, 9, 22), date(2023, 9, 23)])
+        == "[738785, 738786]"
+    )
+
+
+def test_pre_decoder_func():
+    decoder = JSONDecoder(
+        List[date],
+        pre_decoder_func=lambda v: json.loads(
+            "[" + ",".join(f'"{v}"' for v in v.split(",")) + "]"
+        ),
+    )
+    assert decoder.decode("2023-09-22,2023-09-23") == [
+        date(2023, 9, 22),
+        date(2023, 9, 23),
+    ]
+
+
+def test_post_encoder_func():
+    encoder = JSONEncoder(
+        List[date],
+        post_encoder_func=lambda v: json.dumps(v, separators=(",", ":")),
+    )
+    assert (
+        encoder.encode(
+            [
+                date(2023, 9, 22),
+                date(2023, 9, 23),
+            ]
+        )
+        == '["2023-09-22","2023-09-23"]'
+    )
diff --git a/tests/test_codecs/test_msgpack_codec.py b/tests/test_codecs/test_msgpack_codec.py
new file mode 100644
index 00000000..745a8517
--- /dev/null
+++ b/tests/test_codecs/test_msgpack_codec.py
@@ -0,0 +1,97 @@
+from datetime import date
+from typing import List
+
+import msgpack
+
+from mashumaro.codecs.msgpack import (
+    MessagePackDecoder,
+    MessagePackEncoder,
+    msgpack_decode,
+    msgpack_encode,
+)
+from mashumaro.dialect import Dialect
+
+
+class MyDialect(Dialect):
+    serialization_strategy = {
+        date: {
+            "serialize": date.toordinal,
+            "deserialize": date.fromordinal,
+        },
+    }
+
+
+def test_msgpack_decode():
+    data = msgpack.dumps(["2023-09-22", "2023-09-23"])
+    assert msgpack_decode(data, List[date]) == [
+        date(2023, 9, 22),
+        date(2023, 9, 23),
+    ]
+
+
+def test_msgpack_encode():
+    data = msgpack.dumps(["2023-09-22", "2023-09-23"])
+    assert (
+        msgpack_encode([date(2023, 9, 22), date(2023, 9, 23)], List[date])
+        == data
+    )
+
+
+def test_decoder_with_default_dialect():
+    data = msgpack.dumps([738785, 738786])
+    decoder = MessagePackDecoder(List[date], default_dialect=MyDialect)
+    assert decoder.decode(data) == [
+        date(2023, 9, 22),
+        date(2023, 9, 23),
+    ]
+
+
+def test_encoder_with_default_dialect():
+    data = msgpack.dumps([738785, 738786])
+    encoder = MessagePackEncoder(List[date], default_dialect=MyDialect)
+    assert encoder.encode([date(2023, 9, 22), date(2023, 9, 23)]) == data
+
+
+def test_pre_decoder_func():
+    data = msgpack.dumps(["2023-09-22", "2023-09-23"])
+    calls = 0
+
+    def pre_decoder_func(value):
+        nonlocal calls
+        calls += 1
+        return msgpack.loads(value)
+
+    decoder = MessagePackDecoder(
+        List[date],
+        pre_decoder_func=pre_decoder_func,
+    )
+    assert decoder.decode(data) == [
+        date(2023, 9, 22),
+        date(2023, 9, 23),
+    ]
+    assert calls == 1
+
+
+def test_post_encoder_func():
+    data = msgpack.dumps(["2023-09-22", "2023-09-23"])
+    calls = 0
+
+    def post_encoder_func(value):
+        nonlocal calls
+        calls += 1
+        return msgpack.dumps(value)
+
+    encoder = MessagePackEncoder(
+        List[date],
+        post_encoder_func=post_encoder_func,
+    )
+    assert (
+        encoder.encode(
+            [
+                date(2023, 9, 22),
+                date(2023, 9, 23),
+            ]
+        )
+        == data
+    )
+    assert calls == 1
diff --git a/tests/test_codecs/test_orjson_codec.py b/tests/test_codecs/test_orjson_codec.py
new file mode 100644
index 00000000..314b29da
--- /dev/null
+++ b/tests/test_codecs/test_orjson_codec.py
@@ -0,0 +1,49 @@
+from datetime import date
+from typing import List
+
+from mashumaro.codecs.orjson import (
+    ORJSONDecoder,
+    ORJSONEncoder,
+    json_decode,
+    json_encode,
+)
+from mashumaro.dialect import Dialect
+
+
+class MyDialect(Dialect):
+    serialization_strategy = {
+        date: {
+            "serialize": date.toordinal,
+            "deserialize": date.fromordinal,
+        },
+    }
+
+
+def test_json_decode():
+    assert json_decode('["2023-09-22", "2023-09-23"]', List[date]) == [
+        date(2023, 9, 22),
+        date(2023, 9, 23),
+    ]
+
+
+def test_json_encode():
+    assert (
+        json_encode([date(2023, 9, 22), date(2023, 9, 23)], List[date])
+        == b'["2023-09-22","2023-09-23"]'
+    )
+
+
+def test_decoder_with_default_dialect():
+    decoder = ORJSONDecoder(List[date], default_dialect=MyDialect)
+    assert decoder.decode("[738785, 738786]") == [
+        date(2023, 9, 22),
+        date(2023, 9, 23),
+    ]
+
+
+def test_encoder_with_default_dialect():
+    encoder = ORJSONEncoder(List[date], default_dialect=MyDialect)
+    assert (
+        encoder.encode([date(2023, 9, 22), date(2023, 9, 23)])
+        == b"[738785,738786]"
+    )
diff --git a/tests/test_codecs/test_toml_codec.py b/tests/test_codecs/test_toml_codec.py
new file mode 100644
index 00000000..9947c6b6
--- /dev/null
+++ b/tests/test_codecs/test_toml_codec.py
@@ -0,0 +1,61 @@
+from datetime import date
+from typing import Dict, List
+
+import tomli_w
+
+from mashumaro.codecs.toml import (
+    TOMLDecoder,
+    TOMLEncoder,
+    toml_decode,
+    toml_encode,
+)
+from mashumaro.dialect import Dialect
+
+
+class MyDialect(Dialect):
+    serialization_strategy = {
+        date: {
+            "serialize": date.toordinal,
+            "deserialize": date.fromordinal,
+        },
+    }
+
+
+def test_toml_decode():
+    data = tomli_w.dumps({"x": [date(2023, 9, 22), date(2023, 9, 23)]})
+    assert toml_decode(data, Dict[str, List[date]]) == {
+        "x": [
+            date(2023, 9, 22),
+            date(2023, 9, 23),
+        ]
+    }
+
+
+def test_toml_encode():
+    data = tomli_w.dumps({"x": [date(2023, 9, 22), date(2023, 9, 23)]})
+    assert (
+        toml_encode(
+            {"x": [date(2023, 9, 22), date(2023, 9, 23)]},
+            Dict[str, List[date]],
+        )
+        == data
+    )
+
+
+def test_decoder_with_default_dialect():
+    data = tomli_w.dumps({"x": [738785, 738786]})
+    decoder = TOMLDecoder(Dict[str, List[date]], default_dialect=MyDialect)
+    assert decoder.decode(data) == {
+        "x": [
+            date(2023, 9, 22),
+            date(2023, 9, 23),
+        ]
+    }
+
+
+def test_encoder_with_default_dialect():
+    data = tomli_w.dumps({"x": [738785, 738786]})
+    encoder = TOMLEncoder(Dict[str, List[date]], default_dialect=MyDialect)
+    assert (
+        encoder.encode({"x": [date(2023, 9, 22), date(2023, 9, 23)]}) == data
+    )
diff --git a/tests/test_codecs/test_yaml_codec.py b/tests/test_codecs/test_yaml_codec.py
new file mode 100644
index 00000000..276f7eed
--- /dev/null
+++ b/tests/test_codecs/test_yaml_codec.py
@@ -0,0 +1,96 @@
+from datetime import date
+from typing import List
+
+import yaml
+
+from mashumaro.codecs.yaml import (
+    YAMLDecoder,
+    YAMLEncoder,
+    yaml_decode,
+    yaml_encode,
+)
+from mashumaro.dialect import Dialect
+
+
+class MyDialect(Dialect):
+    serialization_strategy = {
+        date: {
+            "serialize": date.toordinal,
+            "deserialize": date.fromordinal,
+        },
+    }
+
+
+def test_yaml_decode():
+    data = "- '2023-09-22'\n- '2023-09-23'\n"
+    assert yaml_decode(data, List[date]) == [
+        date(2023, 9, 22),
+        date(2023, 9, 23),
+    ]
+
+
+def test_yaml_encode():
+    data = "- '2023-09-22'\n- '2023-09-23'\n"
+    assert (
+        yaml_encode([date(2023, 9, 22), date(2023, 9, 23)], List[date]) == data
+    )
+
+
+def test_decoder_with_default_dialect():
+    data = "- 738785\n- 738786\n"
+    decoder = YAMLDecoder(List[date], default_dialect=MyDialect)
+    assert decoder.decode(data) == [
+        date(2023, 9, 22),
+        date(2023, 9, 23),
+    ]
+
+
+def test_encoder_with_default_dialect():
+    data = "- 738785\n- 738786\n"
+    encoder = YAMLEncoder(List[date], default_dialect=MyDialect)
+    assert encoder.encode([date(2023, 9, 22), date(2023, 9, 23)]) == data
+
+
+def test_pre_decoder_func():
+    data = "- '2023-09-22'\n- '2023-09-23'\n"
+    calls = 0
+
+    def pre_decoder_func(value):
+        nonlocal calls
+        calls += 1
+        return yaml.load(value, getattr(yaml, "CSafeLoader", yaml.SafeLoader))
+
+    decoder = YAMLDecoder(
+        List[date],
+        pre_decoder_func=pre_decoder_func,
+    )
+    assert decoder.decode(data) == [
+        date(2023, 9, 22),
+        date(2023, 9, 23),
+    ]
+    assert calls == 1
+
+
+def test_post_encoder_func():
+    data = "- '2023-09-22'\n- '2023-09-23'\n"
+    calls = 0
+
+    def post_encoder_func(value):
+        nonlocal calls
+        calls += 1
+        return yaml.dump(value, Dumper=getattr(yaml, "CDumper", yaml.Dumper))
+
+    encoder = YAMLEncoder(
+        List[date],
+        post_encoder_func=post_encoder_func,
+    )
+    assert (
+        encoder.encode(
+            [
+                date(2023, 9, 22),
+                date(2023, 9, 23),
+            ]
+        )
+        == data
+    )
+    assert calls == 1
diff --git a/tests/test_data_types.py b/tests/test_data_types.py
index 9d9f225b..6055ca9c 100644
--- a/tests/test_data_types.py
+++ b/tests/test_data_types.py
@@ -43,7 +43,9 @@
 from typing_extensions import Final, LiteralString
 
 from mashumaro import DataClassDictMixin
-from mashumaro.config import BaseConfig, TO_DICT_ADD_BY_ALIAS_FLAG
+from mashumaro.codecs import BasicDecoder, BasicEncoder
+from mashumaro.codecs.basic import decode, encode
+from mashumaro.config import BaseConfig
 from mashumaro.core.const import PEP_585_COMPATIBLE, PY_39_MIN
 from mashumaro.exceptions import (
     InvalidFieldValue,
@@ -1222,6 +1224,11 @@ class DataClass(DataClassDictMixin):
     assert DataClass.from_dict({"x": ["1", "2.0"]}) == obj
     assert obj.to_dict() == {"x": [1, 2.0]}
 
+    decoder = BasicDecoder(DataClass)
+    encoder = BasicEncoder(DataClass)
+    assert decoder.decode({"x": ["1", "2.0"]}) == obj
+    assert encoder.encode(obj) == {"x": [1, 2.0]}
+
 
 def test_dataclass_with_named_tuple_with_defaults():
     @dataclass
@@ -1232,6 +1239,11 @@ class DataClass(DataClassDictMixin):
     assert DataClass.from_dict({"x": ["1"]}) == obj
     assert obj.to_dict() == {"x": [1, 2.0]}
 
+    decoder = BasicDecoder(DataClass)
+    encoder = BasicEncoder(DataClass)
+    assert decoder.decode({"x": ["1"]}) == obj
+    assert encoder.encode(obj) == {"x": [1, 2.0]}
+
 
 def test_dataclass_with_untyped_named_tuple():
     @dataclass
@@ -1242,6 +1254,11 @@ class DataClass(DataClassDictMixin):
     assert DataClass.from_dict({"x": ["1", "2.0"]}) == obj
     assert obj.to_dict() == {"x": ["1", "2.0"]}
 
+    decoder = BasicDecoder(DataClass)
+    encoder = BasicEncoder(DataClass)
+    assert decoder.decode({"x": ["1", "2.0"]}) == obj
+    assert encoder.encode(obj) == {"x": ["1", "2.0"]}
+
 
 def test_dataclass_with_untyped_named_tuple_with_defaults():
     @dataclass
@@ -1362,7 +1379,12 @@ class DataClass(DataClassDictMixin):
 
     obj = DataClass({"x": 33, "y": 42})
     assert DataClass.from_dict({"x": {"x": 33, "y": 42}}) == obj
-    assert obj.to_dict()
+    assert obj.to_dict() == {"x": {"x": 33, "y": 42}}
+
+    decoder = BasicDecoder(DataClass)
+    encoder = BasicEncoder(DataClass)
+    assert decoder.decode({"x": {"x": 33, "y": 42}}) == obj
+    assert encoder.encode(obj) == {"x": {"x": 33, "y": 42}}
 
 
 def test_typed_dict_optional_keys_with_optional():
@@ -1376,7 +1398,12 @@ class DataClass(DataClassDictMixin):
 
     obj = DataClass({"x": 33, "y": 42})
     assert DataClass.from_dict({"x": {"x": 33, "y": 42}}) == obj
-    assert obj.to_dict()
+    assert obj.to_dict() == {"x": {"x": 33, "y": 42}}
+
+    decoder = BasicDecoder(DataClass)
+    encoder = BasicEncoder(DataClass)
+    assert decoder.decode({"x": {"x": 33, "y": 42}}) == obj
+    assert encoder.encode(obj) == {"x": {"x": 33, "y": 42}}
 
 
 def test_unbound_generic_typed_dict():
@@ -1388,6 +1415,11 @@ class DataClass(DataClassDictMixin):
     assert DataClass.from_dict({"x": {"x": "2023-01-22", "y": "42"}}) == obj
     assert obj.to_dict() == {"x": {"x": "2023-01-22", "y": 42}}
 
+    decoder = BasicDecoder(DataClass)
+    encoder = BasicEncoder(DataClass)
+    assert decoder.decode({"x": {"x": "2023-01-22", "y": "42"}}) == obj
+    assert encoder.encode(obj) == {"x": {"x": "2023-01-22", "y": 42}}
+
 
 def test_bound_generic_typed_dict():
     @dataclass
@@ -1398,6 +1430,11 @@ class DataClass(DataClassDictMixin):
     assert DataClass.from_dict({"x": {"x": "2023-01-22", "y": "42"}}) == obj
     assert obj.to_dict() == {"x": {"x": "2023-01-22", "y": 42}}
 
+    decoder = BasicDecoder(DataClass)
+    encoder = BasicEncoder(DataClass)
+    assert decoder.decode({"x": {"x": "2023-01-22", "y": "42"}}) == obj
+    assert encoder.encode(obj) == {"x": {"x": "2023-01-22", "y": 42}}
+
 
 def test_dataclass_with_init_false_field():
     @dataclass
@@ -1462,3 +1499,19 @@ class Config(BaseConfig):
         "b": float("-inf"),
         "c": float("+inf"),
     }
+
+
+@pytest.mark.parametrize("value_info", inner_values)
+def test_decoder(value_info):
+    x_type, x_value, x_value_dumped = value_info
+    decoder = BasicDecoder(x_type)
+    assert decoder.decode(x_value_dumped) == x_value
+    assert decode(x_value_dumped, x_type) == x_value
+
+
+@pytest.mark.parametrize("value_info", inner_values)
+def test_encoder(value_info):
+    x_type, x_value, x_value_dumped = value_info
+    encoder = BasicEncoder(x_type)
+    assert encoder.encode(x_value) == x_value_dumped
+    assert encode(x_value, x_type) == x_value_dumped
diff --git a/tests/test_dialect.py b/tests/test_dialect.py
index d7478535..e0ba6cdc 100644
--- a/tests/test_dialect.py
+++ b/tests/test_dialect.py
@@ -1200,3 +1200,52 @@ class Config(BaseConfig):
     assert data["e"] is not obj.e
     assert data["f"] is obj.f
     assert data["g"] is not obj.g
+
+
+def test_dialect_merge():
+    class DialectA(Dialect):
+        omit_none = True
+        omit_default = True
+        no_copy_collections = [set, dict]
+        serialization_strategy = {
+            date: {
+                "serialize": date.toordinal,
+                "deserialize": date.fromordinal,
+            },
+            int: HexSerializationStrategy(),
+            float: {
+                "serialize": pass_through,
+                "deserialize": float,
+            },
+        }
+
+    class DialectB(Dialect):
+        omit_none = False
+        omit_default = False
+        no_copy_collections = [list]
+        serialization_strategy = {
+            date: pass_through,
+            int: {
+                "serialize": int,
+                "deserialize": int,
+            },
+            float: {
+                "serialize": float,
+            },
+        }
+
+    DialectC = DialectA.merge(DialectB)
+    assert DialectC.omit_none is False
+    assert DialectC.omit_default is False
+    assert DialectC.no_copy_collections == [list]
+    assert DialectC.serialization_strategy == {
+        date: pass_through,
+        int: {
+            "serialize": int,
+            "deserialize": int,
+        },
+        float: {
+            "serialize": float,
+            "deserialize": float,
+        },
+    }
diff --git a/tests/test_discriminated_unions/test_dialects.py b/tests/test_discriminated_unions/test_dialects.py
index d8760c3d..ab966cdf 100644
--- a/tests/test_discriminated_unions/test_dialects.py
+++ b/tests/test_discriminated_unions/test_dialects.py
@@ -4,6 +4,8 @@
 
 from typing_extensions import Annotated, Literal
 
+from mashumaro.codecs import BasicDecoder
+from mashumaro.codecs.orjson import ORJSONDecoder
 from mashumaro.config import ADD_DIALECT_SUPPORT, BaseConfig
 from mashumaro.dialect import Dialect
 from mashumaro.mixins.orjson import DataClassORJSONMixin
@@ -47,11 +49,25 @@ class Config(BaseConfig):
         discriminator = Discriminator(field="type", include_subtypes=True)
 
 
+@dataclass
+class _Variant1:
+    x: date
+    y: Dict[str, int]
+
+    class Config(BaseConfig):
+        discriminator = Discriminator(field="type", include_subtypes=True)
+
+
 @dataclass
 class Variant1Subtype1(Variant1):
     type: Literal[1] = 1
 
 
+@dataclass
+class _Variant1Subtype1(_Variant1):
+    type: Literal[1] = 1
+
+
 @dataclass
 class Variant2(DataClassORJSONMixin):
     x: date
@@ -62,11 +78,25 @@ class Config(BaseConfig):
         dialect = DefaultDialect
 
 
+@dataclass
+class _Variant2:
+    x: date
+    y: Dict[str, int]
+
+    class Config(BaseConfig):
+        dialect = DefaultDialect
+
+
 @dataclass
 class Variant2Subtype1(Variant2):
     type: Literal[1] = 1
 
 
+@dataclass
+class _Variant2Subtype1(_Variant2):
+    type: Literal[1] = 1
+
+
 @dataclass
 class Variant2Wrapper(DataClassORJSONMixin):
     x: Annotated[Variant2, Discriminator(field="type", include_subtypes=True)]
@@ -75,6 +105,14 @@ class Config(BaseConfig):
         code_generation_options = [ADD_DIALECT_SUPPORT]
 
 
+@dataclass
+class _Variant2Wrapper:
+    x: Annotated[_Variant2, Discriminator(field="type", include_subtypes=True)]
+
+    class Config(BaseConfig):
+        code_generation_options = [ADD_DIALECT_SUPPORT]
+
+
 @dataclass
 class Variant3(DataClassORJSONMixin):
     x: date
@@ -85,11 +123,26 @@ class Config(BaseConfig):
         dialect = DefaultDialect
 
 
+@dataclass
+class _Variant3:
+    x: date
+    y: Dict[str, int]
+
+    class Config(BaseConfig):
+        code_generation_options = [ADD_DIALECT_SUPPORT]
+        dialect = DefaultDialect
+
+
 @dataclass
 class Variant3Subtype(Variant3):
     type: Literal[3] = 3
 
 
+@dataclass
+class _Variant3Subtype(_Variant3):
+    type: Literal[3] = 3
+
+
 @dataclass
 class Variant4(DataClassORJSONMixin):
     x: date
@@ -100,11 +153,26 @@ class Config(BaseConfig):
         dialect = DefaultDialect
 
 
+@dataclass
+class _Variant4:
+    x: date
+    y: Dict[str, int]
+
+    class Config(BaseConfig):
+        code_generation_options = [ADD_DIALECT_SUPPORT]
+        dialect = DefaultDialect
+
+
 @dataclass
 class Variant4Subtype(Variant4):
     type: Literal[4] = 4
 
 
+@dataclass
+class _Variant4Subtype(_Variant4):
+    type: Literal[4] = 4
+
+
 @dataclass
 class Variant34Wrapper(DataClassORJSONMixin):
     x: Annotated[
@@ -116,163 +184,294 @@ class Config(BaseConfig):
         code_generation_options = [ADD_DIALECT_SUPPORT]
 
 
+@dataclass
+class _Variant34Wrapper:
+    x: Annotated[
+        Union[_Variant3, _Variant4],
+        Discriminator(field="type", include_subtypes=True),
+    ]
+
+    class Config(BaseConfig):
+        code_generation_options = [ADD_DIALECT_SUPPORT]
+
+
 def test_passing_dialect_to_config_based_variant_subtypes():
     assert Variant1.from_dict(
         {"type": 1, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}
     ) == Variant1Subtype1(date(2023, 6, 3), {"1": 2, "3": 4})
+    decoder1 = BasicDecoder(_Variant1, default_dialect=DefaultDialect)
+    assert decoder1.decode(
+        {"type": 1, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}
+    ) == _Variant1Subtype1(date(2023, 6, 3), {"1": 2, "3": 4})
 
     assert Variant1.from_json(
         '{"type": 1, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}'
     ) == Variant1Subtype1(date(2023, 6, 3), {"1": 2, "3": 4})
+    decoder2 = ORJSONDecoder(_Variant1, default_dialect=DefaultDialect)
+    assert decoder2.decode(
+        '{"type": 1, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}'
+    ) == _Variant1Subtype1(date(2023, 6, 3), {"1": 2, "3": 4})
 
     assert Variant1.from_dict(
         {"type": 1, "x": 738674, "y": {"1": 2, "3": 4}}, dialect=MyDialect
     ) == Variant1Subtype1(date(2023, 6, 3), {"1": 2, "3": 4})
+    decoder3 = BasicDecoder(_Variant1, default_dialect=MyDialect)
+    assert decoder3.decode(
+        {"type": 1, "x": 738674, "y": {"1": 2, "3": 4}}
+    ) == _Variant1Subtype1(date(2023, 6, 3), {"1": 2, "3": 4})
 
     assert Variant1.from_json(
         '{"type": 1, "x": 738674, "y": {"1": 2, "3": 4}}', dialect=MyDialect
     ) == Variant1Subtype1(date(2023, 6, 3), {"1": 2, "3": 4})
+    decoder4 = ORJSONDecoder(_Variant1, default_dialect=MyDialect)
+    assert decoder4.decode(
+        '{"type": 1, "x": 738674, "y": {"1": 2, "3": 4}}'
+    ) == _Variant1Subtype1(date(2023, 6, 3), {"1": 2, "3": 4})
 
     @dataclass
     class Variant1Subtype2(Variant1):
         type: Literal[2] = 2
 
+    @dataclass
+    class _Variant1Subtype2(_Variant1):
+        type: Literal[2] = 2
+
     assert Variant1.from_dict(
         {"type": 2, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}
     ) == Variant1Subtype2(date(2023, 6, 3), {"1": 2, "3": 4})
+    assert decoder1.decode(
+        {"type": 2, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}
+    ) == _Variant1Subtype2(date(2023, 6, 3), {"1": 2, "3": 4})
 
     assert Variant1.from_json(
         '{"type": 2, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}'
     ) == Variant1Subtype2(date(2023, 6, 3), {"1": 2, "3": 4})
+    assert decoder2.decode(
+        '{"type": 2, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}'
+    ) == _Variant1Subtype2(date(2023, 6, 3), {"1": 2, "3": 4})
 
     assert Variant1.from_dict(
         {"type": 2, "x": 738674, "y": {"1": 2, "3": 4}}, dialect=MyDialect
     ) == Variant1Subtype2(date(2023, 6, 3), {"1": 2, "3": 4})
+    assert decoder3.decode(
+        {"type": 2, "x": 738674, "y": {"1": 2, "3": 4}}
+    ) == _Variant1Subtype2(date(2023, 6, 3), {"1": 2, "3": 4})
 
     assert Variant1.from_json(
         '{"type": 2, "x": 738674, "y": {"1": 2, "3": 4}}', dialect=MyDialect
     ) == Variant1Subtype2(date(2023, 6, 3), {"1": 2, "3": 4})
+    assert decoder4.decode(
+        '{"type": 2, "x": 738674, "y": {"1": 2, "3": 4}}'
+    ) == _Variant1Subtype2(date(2023, 6, 3), {"1": 2, "3": 4})
 
 
 def test_passing_dialect_to_annotation_based_variant_subtypes():
     assert Variant2Wrapper.from_dict(
         {"x": {"type": 1, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}
     ) == Variant2Wrapper(Variant2Subtype1(date(2023, 6, 3), {"1": 2, "3": 4}))
+    decoder1 = BasicDecoder(_Variant2Wrapper)
+    assert decoder1.decode(
+        {"x": {"type": 1, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}
+    ) == _Variant2Wrapper(
+        _Variant2Subtype1(date(2023, 6, 3), {"1": 2, "3": 4})
+    )
 
     assert Variant2Wrapper.from_json(
         '{"x": {"type": 1, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}'
     ) == Variant2Wrapper(Variant2Subtype1(date(2023, 6, 3), {"1": 2, "3": 4}))
+    decoder2 = ORJSONDecoder(_Variant2Wrapper)
+    assert decoder2.decode(
+        '{"x": {"type": 1, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}'
+    ) == _Variant2Wrapper(
+        _Variant2Subtype1(date(2023, 6, 3), {"1": 2, "3": 4})
+    )
 
     assert Variant2Wrapper.from_dict(
         {"x": {"type": 1, "x": 738674, "y": {"1": 2, "3": 4}}},
         dialect=MyDialect,
     ) == Variant2Wrapper(Variant2Subtype1(date(2023, 6, 3), {"1": 2, "3": 4}))
+    # "decode" doesn't accept a dialect, so we can't pass MyDialect into it
 
     assert Variant2Wrapper.from_json(
         '{"x": {"type": 1, "x": 738674, "y": {"1": 2, "3": 4}}}',
         dialect=MyDialect,
     ) == Variant2Wrapper(Variant2Subtype1(date(2023, 6, 3), {"1": 2, "3": 4}))
+    # "decode" doesn't accept a dialect, so we can't pass MyDialect into it
 
     @dataclass
     class Variant2Subtype2(Variant2):
         type: Literal[2] = 2
 
+    @dataclass
+    class _Variant2Subtype2(_Variant2):
+        type: Literal[2] = 2
+
     assert Variant2Wrapper.from_dict(
         {"x": {"type": 2, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}
     ) == Variant2Wrapper(Variant2Subtype2(date(2023, 6, 3), {"1": 2, "3": 4}))
+    assert decoder1.decode(
+        {"x": {"type": 2, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}
+    ) == _Variant2Wrapper(
+        _Variant2Subtype2(date(2023, 6, 3), {"1": 2, "3": 4})
+    )
 
     assert Variant2Wrapper.from_json(
         '{"x": {"type": 2, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}'
     ) == Variant2Wrapper(Variant2Subtype2(date(2023, 6, 3), {"1": 2, "3": 4}))
+    assert decoder2.decode(
+        '{"x": {"type": 2, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}'
+    ) == _Variant2Wrapper(
+        _Variant2Subtype2(date(2023, 6, 3), {"1": 2, "3": 4})
+    )
 
     assert Variant2Wrapper.from_dict(
         {"x": {"type": 2, "x": 738674, "y": {"1": 2, "3": 4}}},
         dialect=MyDialect,
     ) == Variant2Wrapper(Variant2Subtype2(date(2023, 6, 3), {"1": 2, "3": 4}))
+    # "decode" doesn't accept a dialect, so we can't pass MyDialect into it
 
     assert Variant2Wrapper.from_json(
         '{"x": {"type": 2, "x": 738674, "y": {"1": 2, "3": 4}}}',
         dialect=MyDialect,
     ) == Variant2Wrapper(Variant2Subtype2(date(2023, 6, 3), {"1": 2, "3": 4}))
+    # "decode" doesn't accept a dialect, so we can't pass MyDialect into it
 
 
 def test_passing_dialect_to_annotation_based_union_subtypes():
     assert Variant34Wrapper.from_dict(
         {"x": {"type": 3, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}
     ) == Variant34Wrapper(Variant3Subtype(date(2023, 6, 3), {"1": 2, "3": 4}))
+    decoder1 = BasicDecoder(_Variant34Wrapper)
+    assert decoder1.decode(
+        {"x": {"type": 3, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}
+    ) == _Variant34Wrapper(
+        _Variant3Subtype(date(2023, 6, 3), {"1": 2, "3": 4})
+    )
 
     assert Variant34Wrapper.from_dict(
         {"x": {"type": 4, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}
     ) == Variant34Wrapper(Variant4Subtype(date(2023, 6, 3), {"1": 2, "3": 4}))
+    assert decoder1.decode(
+        {"x": {"type": 4, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}
+    ) == _Variant34Wrapper(
+        _Variant4Subtype(date(2023, 6, 3), {"1": 2, "3": 4})
+    )
 
     assert Variant34Wrapper.from_json(
         '{"x": {"type": 3, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}'
     ) == Variant34Wrapper(Variant3Subtype(date(2023, 6, 3), {"1": 2, "3": 4}))
+    decoder2 = ORJSONDecoder(_Variant34Wrapper)
+    assert decoder2.decode(
+        '{"x": {"type": 3, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}'
+    ) == _Variant34Wrapper(
+        _Variant3Subtype(date(2023, 6, 3), {"1": 2, "3": 4})
+    )
 
     assert Variant34Wrapper.from_json(
         '{"x": {"type": 4, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}'
     ) == Variant34Wrapper(Variant4Subtype(date(2023, 6, 3), {"1": 2, "3": 4}))
+    assert decoder2.decode(
+        '{"x": {"type": 4, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}'
+    ) == _Variant34Wrapper(
+        _Variant4Subtype(date(2023, 6, 3), {"1": 2, "3": 4})
+    )
 
     assert Variant34Wrapper.from_dict(
         {"x": {"type": 3, "x": 738674, "y": {"1": 2, "3": 4}}},
         dialect=MyDialect,
     ) == Variant34Wrapper(Variant3Subtype(date(2023, 6, 3), {"1": 2, "3": 4}))
+    # "decode" doesn't accept a dialect, so we can't pass MyDialect into it
 
     assert Variant34Wrapper.from_dict(
         {"x": {"type": 4, "x": 738674, "y": {"1": 2, "3": 4}}},
         dialect=MyDialect,
     ) == Variant34Wrapper(Variant4Subtype(date(2023, 6, 3), {"1": 2, "3": 4}))
+    # "decode" doesn't accept a dialect, so we can't pass MyDialect into it
 
     assert Variant34Wrapper.from_json(
         '{"x": {"type": 3, "x": 738674, "y": {"1": 2, "3": 4}}}',
         dialect=MyDialect,
     ) == Variant34Wrapper(Variant3Subtype(date(2023, 6, 3), {"1": 2, "3": 4}))
+    # "decode" doesn't accept a dialect, so we can't pass MyDialect into it
 
     assert Variant34Wrapper.from_json(
         '{"x": {"type": 4, "x": 738674, "y": {"1": 2, "3": 4}}}',
         dialect=MyDialect,
     ) == Variant34Wrapper(Variant4Subtype(date(2023, 6, 3), {"1": 2, "3": 4}))
+    # "decode" doesn't accept a dialect, so we can't pass MyDialect into it
 
     @dataclass
     class Variant3Subtype2(Variant3):
         type: Literal[5] = 5
 
+    @dataclass
+    class _Variant3Subtype2(_Variant3):
+        type: Literal[5] = 5
+
     @dataclass
     class Variant4Subtype2(Variant4):
         type: Literal[6] = 6
 
+    @dataclass
+    class _Variant4Subtype2(_Variant4):
+        type: Literal[6] = 6
+
     assert Variant34Wrapper.from_dict(
         {"x": {"type": 5, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}
     ) == Variant34Wrapper(Variant3Subtype2(date(2023, 6, 3), {"1": 2, "3": 4}))
+    assert decoder1.decode(
+        {"x": {"type": 5, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}
+    ) == _Variant34Wrapper(
+        _Variant3Subtype2(date(2023, 6, 3), {"1": 2, "3": 4})
+    )
 
     assert Variant34Wrapper.from_dict(
         {"x": {"type": 6, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}
     ) == Variant34Wrapper(Variant4Subtype2(date(2023, 6, 3), {"1": 2, "3": 4}))
+    assert decoder1.decode(
+        {"x": {"type": 6, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}
+    ) == _Variant34Wrapper(
+        _Variant4Subtype2(date(2023, 6, 3), {"1": 2, "3": 4})
+    )
 
     assert Variant34Wrapper.from_json(
         '{"x": {"type": 5, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}'
     ) == Variant34Wrapper(Variant3Subtype2(date(2023, 6, 3), {"1": 2, "3": 4}))
+    assert decoder2.decode(
+        '{"x": {"type": 5, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}'
+    ) == _Variant34Wrapper(
+        _Variant3Subtype2(date(2023, 6, 3), {"1": 2, "3": 4})
+    )
 
     assert Variant34Wrapper.from_json(
         '{"x": {"type": 6, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}'
     ) == Variant34Wrapper(Variant4Subtype2(date(2023, 6, 3), {"1": 2, "3": 4}))
+    assert decoder2.decode(
+        '{"x": {"type": 6, "x": "2023-06-03", "y": [["1", 2], ["3", 4]]}}'
+    ) == _Variant34Wrapper(
+        _Variant4Subtype2(date(2023, 6, 3), {"1": 2, "3": 4})
+    )
 
     assert Variant34Wrapper.from_dict(
         {"x": {"type": 5, "x": 738674, "y": {"1": 2, "3": 4}}},
         dialect=MyDialect,
     ) == Variant34Wrapper(Variant3Subtype2(date(2023, 6, 3), {"1": 2, "3": 4}))
+    # "decode" doesn't accept a dialect, so we can't pass MyDialect into it
 
     assert Variant34Wrapper.from_dict(
         {"x": {"type": 6, "x": 738674, "y": {"1": 2, "3": 4}}},
         dialect=MyDialect,
     ) == Variant34Wrapper(Variant4Subtype2(date(2023, 6, 3), {"1": 2, "3": 4}))
+    # "decode" doesn't accept a dialect, so we can't pass MyDialect into it
 
     assert Variant34Wrapper.from_json(
         '{"x": {"type": 5, "x": 738674, "y": {"1": 2, "3": 4}}}',
         dialect=MyDialect,
     ) == Variant34Wrapper(Variant3Subtype2(date(2023, 6, 3), {"1": 2, "3": 4}))
+    # "decode" doesn't accept a dialect, so we can't pass MyDialect into it
 
     assert Variant34Wrapper.from_json(
         '{"x": {"type": 6, "x": 738674, "y": {"1": 2, "3": 4}}}',
         dialect=MyDialect,
     ) == Variant34Wrapper(Variant4Subtype2(date(2023, 6, 3), {"1": 2, "3": 4}))
+    # "decode" doesn't accept a dialect, so we can't pass MyDialect into it
diff --git a/tests/test_discriminated_unions/test_parent_by_field.py b/tests/test_discriminated_unions/test_parent_by_field.py
index d1a9b5b9..cc076ef7 100644
--- a/tests/test_discriminated_unions/test_parent_by_field.py
+++ b/tests/test_discriminated_unions/test_parent_by_field.py
@@ -5,6 +5,7 @@
 from typing_extensions import Annotated, Literal
 
 from mashumaro import DataClassDictMixin
+from mashumaro.codecs.basic import decode
 from mashumaro.exceptions import InvalidFieldValue
 from mashumaro.types import Discriminator
 
@@ -24,47 +25,93 @@ class BaseVariant(DataClassDictMixin):
     pass
 
 
+@dataclass
+class _BaseVariant:
+    pass
+
+
 @dataclass
 class VariantStr1(BaseVariant):
     x: str
     type: Literal["str_1"] = "str_1"
 
 
+@dataclass
+class _VariantStr1(_BaseVariant):
+    x: str
+    type: Literal["str_1"] = "str_1"
+
+
 @dataclass
 class VariantDate1(BaseVariant):
     x: date
     type: Literal["date_1"] = "date_1"
 
 
+@dataclass
+class _VariantDate1(_BaseVariant):
+    x: date
+    type: Literal["date_1"] = "date_1"
+
+
 @dataclass
 class VariantStr12(VariantStr1):
     x: str
     type: Literal["str_12"] = "str_12"
 
 
+@dataclass
+class _VariantStr12(_VariantStr1):
+    x: str
+    type: Literal["str_12"] = "str_12"
+
+
 @dataclass
 class VariantDate12(VariantStr1):
     x: date
     type: Literal["date_12"] = "date_12"
 
 
+@dataclass
+class _VariantDate12(_VariantStr1):
+    x: date
+    type: Literal["date_12"] = "date_12"
+
+
 @dataclass
 class VariantStr21(VariantDate1):
     x: str
     type: Literal["str_21"] = "str_21"
 
 
+@dataclass
+class _VariantStr21(_VariantDate1):
+    x: str
+    type: Literal["str_21"] = "str_21"
+
+
 @dataclass
 class VariantDate22(VariantDate1):
     x: date
     type: Literal["date_22"] = "date_22"
 
 
+@dataclass
+class _VariantDate22(_VariantDate1):
+    x: date
+    type: Literal["date_22"] = "date_22"
+
+
 @dataclass
 class BySubtypes(DataClassDictMixin):
     x: Annotated[BaseVariant, Discriminator(include_subtypes=True)]
 
 
+@dataclass
+class _BySubtypes:
+    x: Annotated[_BaseVariant, Discriminator(include_subtypes=True)]
+
+
 @dataclass
 class BySupertypes(DataClassDictMixin):
     x: Annotated[BaseVariant, Discriminator(include_supertypes=True)]
@@ -72,6 +119,13 @@ class BySupertypes(DataClassDictMixin):
     x2: Annotated[VariantDate1, Discriminator(include_supertypes=True)]
 
 
+@dataclass
+class _BySupertypes:
+    x: Annotated[_BaseVariant, Discriminator(include_supertypes=True)]
+    x1: Annotated[_VariantStr1, Discriminator(include_supertypes=True)]
+    x2: Annotated[_VariantDate1, Discriminator(include_supertypes=True)]
+
+
 @dataclass
 class ByFieldWithSubtypes(DataClassDictMixin):
     x: Annotated[
@@ -79,6 +133,13 @@ class ByFieldWithSubtypes(DataClassDictMixin):
     ]
 
 
+@dataclass
+class _ByFieldWithSubtypes:
+    x: Annotated[
+        _BaseVariant, Discriminator(field="type", include_subtypes=True)
+    ]
+
+
 @dataclass
 class ByFieldWithSupertypes(DataClassDictMixin):
     x1: Annotated[
@@ -89,6 +150,16 @@ class ByFieldWithSupertypes(DataClassDictMixin):
     ]
 
 
+@dataclass
+class _ByFieldWithSupertypes:
+    x1: Annotated[
+        _VariantStr1, Discriminator(field="type", include_supertypes=True)
+    ]
+    x2: Annotated[
+        _VariantDate1, Discriminator(field="type", include_supertypes=True)
+    ]
+
+
 @dataclass
 class ByFieldWithSupertypesAndSubtypes(DataClassDictMixin):
     x1: Annotated[
@@ -105,6 +176,22 @@ class ByFieldWithSupertypesAndSubtypes(DataClassDictMixin):
     ]
 
 
+@dataclass
+class _ByFieldWithSupertypesAndSubtypes:
+    x1: Annotated[
+        _VariantStr1,
+        Discriminator(
+            field="type", include_supertypes=True, include_subtypes=True
+        ),
+    ]
+    x2: Annotated[
+        _VariantDate1,
+        Discriminator(
+            field="type", include_supertypes=True, include_subtypes=True
+        ),
+    ]
+
+
 @dataclass
 class BySupertypesAndSubtypes(DataClassDictMixin):
     x1: Annotated[
@@ -117,49 +204,104 @@ class BySupertypesAndSubtypes(DataClassDictMixin):
     ]
 
 
+@dataclass
+class _BySupertypesAndSubtypes:
+    x1: Annotated[
+        _VariantStr1,
+        Discriminator(include_supertypes=True, include_subtypes=True),
+    ]
+    x2: Annotated[
+        _VariantDate1,
+        Discriminator(include_supertypes=True, include_subtypes=True),
+    ]
+
+
 @dataclass
 class Foo1(DataClassDictMixin):
     x1: int
 
 
+@dataclass
+class _Foo1:
+    x1: int
+
+
 @dataclass
 class Foo2(Foo1):
     x2: int
 
 
+@dataclass
+class _Foo2(_Foo1):
+    x2: int
+
+
 @dataclass
 class Foo3(Foo2):
     x: int
     type = 3
 
 
+@dataclass
+class _Foo3(_Foo2):
+    x: int
+    type = 3
+
+
 @dataclass
 class Foo4(Foo2):
     x: int
     type = 4
 
 
+@dataclass
+class _Foo4(_Foo2):
+    x: int
+    type = 4
+
+
 @dataclass
 class Bar(DataClassDictMixin):
     baz1: Annotated[Foo1, Discriminator(field="type", include_subtypes=True)]
     baz2: Annotated[Foo1, Discriminator(include_subtypes=True)]
 
 
+@dataclass
+class _Bar:
+    baz1: Annotated[_Foo1, Discriminator(field="type", include_subtypes=True)]
+    baz2: Annotated[_Foo1, Discriminator(include_subtypes=True)]
+
+
 @dataclass
 class BaseVariantWitCustomTagger(DataClassDictMixin):
     pass
 
 
+@dataclass
+class _BaseVariantWitCustomTagger:
+    pass
+
+
 @dataclass
 class VariantWitCustomTaggerSub1(BaseVariantWitCustomTagger):
     pass
 
 
+@dataclass
+class _VariantWitCustomTaggerSub1(_BaseVariantWitCustomTagger):
+    pass
+
+
 @dataclass
 class VariantWitCustomTaggerSub2(BaseVariantWitCustomTagger):
     pass
 
 
+@dataclass
+class _VariantWitCustomTaggerSub2(_BaseVariantWitCustomTagger):
+    pass
+
+
 @dataclass
 class VariantWitCustomTaggerOwner(DataClassDictMixin):
     x: Annotated[
@@ -172,6 +314,18 @@ class VariantWitCustomTaggerOwner(DataClassDictMixin):
     ]
 
 
+@dataclass
+class _VariantWitCustomTaggerOwner:
+    x: Annotated[
+        _BaseVariantWitCustomTagger,
+        Discriminator(
+            field="type",
+            include_subtypes=True,
+            variant_tagger_fn=lambda cls: cls.__name__.lower(),
+        ),
+    ]
+
+
 @pytest.mark.parametrize(
     ["variant_data", "variant"],
     [
@@ -187,9 +341,26 @@ def test_by_subtypes(variant_data, variant):
     assert BySubtypes.from_dict({"x": variant_data}) == BySubtypes(variant)
 
 
+@pytest.mark.parametrize(
+    ["variant_data", "variant"],
+    [
+        (X_STR_1, _VariantStr1(DT_STR)),
+        (X_STR_12, _VariantStr12(DT_STR)),
+        (X_DATE_12, _VariantDate12(DT_DATE)),
+        (X_DATE_1, _VariantDate1(DT_DATE)),
+        (X_STR_21, _VariantStr21(DT_STR)),
+        (X_DATE_22, _VariantDate22(DT_DATE)),
+    ],
+)
+def test_by_subtypes_with_decoder(variant_data, variant):
+    assert decode({"x": variant_data}, _BySubtypes) == _BySubtypes(variant)
+
+
 def test_by_subtypes_exceptions():
     with pytest.raises(InvalidFieldValue):
         BySubtypes.from_dict({"x": {"type": "unknown"}})
+    with pytest.raises(InvalidFieldValue):
+        decode({"x": {"type": "unknown"}}, _BySubtypes)
 
 
 def test_by_supertypes():
@@ -198,21 +369,39 @@ def test_by_supertypes():
     ) == BySupertypes(
         BaseVariant(), VariantStr1(DT_STR), VariantDate1(DT_DATE)
     )
+    assert decode(
+        {"x": {}, "x1": X_STR_1, "x2": X_DATE_1}, _BySupertypes
+    ) == _BySupertypes(
+        _BaseVariant(), _VariantStr1(DT_STR), _VariantDate1(DT_DATE)
+    )
 
     assert BySupertypes.from_dict(
         {"x": X_STR_1, "x1": X_STR_1, "x2": X_DATE_1}
     ) == BySupertypes(
         BaseVariant(), VariantStr1(DT_STR), VariantDate1(DT_DATE)
     )
+    assert decode(
+        {"x": X_STR_1, "x1": X_STR_1, "x2": X_DATE_1}, _BySupertypes
+    ) == _BySupertypes(
+        _BaseVariant(), _VariantStr1(DT_STR), _VariantDate1(DT_DATE)
+    )
 
     with pytest.raises(InvalidFieldValue) as exc_info:
         BySupertypes.from_dict({"x": {}, "x1": X_STR_12, "x2": X_DATE_1})
     assert exc_info.value.field_name == "x1"
 
+    with pytest.raises(InvalidFieldValue) as exc_info:
+        decode({"x": {}, "x1": X_STR_12, "x2": X_DATE_1}, _BySupertypes)
+    assert exc_info.value.field_name == "x1"
+
     with pytest.raises(InvalidFieldValue) as exc_info:
         BySupertypes.from_dict({"x": {}, "x1": X_STR_1, "x2": X_DATE_22})
     assert exc_info.value.field_name == "x2"
 
+    with pytest.raises(InvalidFieldValue) as exc_info:
+        decode({"x": {}, "x1": X_STR_1, "x2": X_DATE_22}, _BySupertypes)
+    assert exc_info.value.field_name == "x2"
+
 
 @pytest.mark.parametrize(
     ["variant_data", "variant"],
@@ -231,24 +420,54 @@ def test_by_field_with_subtypes(variant_data, variant):
     ) == ByFieldWithSubtypes(variant)
 
 
+@pytest.mark.parametrize(
+    ["variant_data", "variant"],
+    [
+        (X_STR_1, _VariantStr1(DT_STR)),
+        (X_STR_12, _VariantStr12(DT_STR)),
+        (X_DATE_12, _VariantDate12(DT_DATE)),
+        (X_DATE_1, _VariantDate1(DT_DATE)),
+        (X_STR_21, _VariantStr21(DT_STR)),
+        (X_DATE_22, _VariantDate22(DT_DATE)),
+    ],
+)
+def test_by_field_with_subtypes_with_decoder(variant_data, variant):
+    assert decode(
+        {"x": variant_data}, _ByFieldWithSubtypes
+    ) == _ByFieldWithSubtypes(variant)
+
+
 def test_by_field_with_subtypes_exceptions():
     with pytest.raises(InvalidFieldValue):
         ByFieldWithSubtypes.from_dict({"x": {"type": "unknown"}})
+    with pytest.raises(InvalidFieldValue):
+        decode({"x": {"type": "unknown"}}, _ByFieldWithSubtypes)
 
 
 def test_by_field_with_supertypes():
     assert ByFieldWithSupertypes.from_dict(
         {"x1": X_STR_1, "x2": X_DATE_1}
     ) == ByFieldWithSupertypes(VariantStr1(DT_STR), VariantDate1(DT_DATE))
+    assert decode(
+        {"x1": X_STR_1, "x2": X_DATE_1}, _ByFieldWithSupertypes
+    ) == _ByFieldWithSupertypes(_VariantStr1(DT_STR), _VariantDate1(DT_DATE))
 
     with pytest.raises(InvalidFieldValue) as exc_info:
         ByFieldWithSupertypes.from_dict({"x1": X_STR_12, "x2": X_DATE_1})
     assert exc_info.value.field_name == "x1"
 
+    with pytest.raises(InvalidFieldValue) as exc_info:
+        decode({"x1": X_STR_12, "x2": X_DATE_1}, _ByFieldWithSupertypes)
+    assert exc_info.value.field_name == "x1"
+
     with pytest.raises(InvalidFieldValue) as exc_info:
         ByFieldWithSupertypes.from_dict({"x1": X_STR_1, "x2": X_DATE_22})
     assert exc_info.value.field_name == "x2"
 
+    with pytest.raises(InvalidFieldValue) as exc_info:
+        decode({"x1": X_STR_1, "x2": X_DATE_22}, _ByFieldWithSupertypes)
+    assert exc_info.value.field_name == "x2"
+
 
 @pytest.mark.parametrize(
     ["dataclass_cls"],
@@ -277,6 +496,33 @@ def test_by_field_with_supertypes_and_subtypes(dataclass_cls):
         dataclass_cls.from_dict({"x1": X_DATE_22, "x2": X_DATE_12})
 
 
+@pytest.mark.parametrize(
+    ["dataclass_cls"],
+    [[_ByFieldWithSupertypesAndSubtypes], [_BySupertypesAndSubtypes]],
+)
+def test_by_field_with_supertypes_and_subtypes_with_decoder(dataclass_cls):
+    assert decode(
+        {"x1": X_STR_1, "x2": X_DATE_1}, dataclass_cls
+    ) == dataclass_cls(_VariantStr1(DT_STR), _VariantDate1(DT_DATE))
+
+    assert decode(
+        {"x1": X_STR_12, "x2": X_STR_21}, dataclass_cls
+    ) == dataclass_cls(_VariantStr12(DT_STR), _VariantStr21(DT_STR))
+
+    assert decode(
+        {"x1": X_DATE_12, "x2": X_DATE_22}, dataclass_cls
+    ) == dataclass_cls(_VariantDate12(DT_DATE), _VariantDate22(DT_DATE))
+
+    with pytest.raises(InvalidFieldValue):
+        decode({"x1": X_DATE_1, "x2": X_STR_1}, dataclass_cls)
+
+    with pytest.raises(InvalidFieldValue):
+        decode({"x1": X_STR_21, "x2": X_STR_12}, dataclass_cls)
+
+    with pytest.raises(InvalidFieldValue):
+        decode({"x1": X_DATE_22, "x2": X_DATE_12}, dataclass_cls)
+
+
 def test_subclass_tree_with_class_without_field():
     assert Bar.from_dict(
         {
@@ -284,14 +530,33 @@ def test_subclass_tree_with_class_without_field():
             "baz2": {"type": 4, "x1": 1, "x2": 2, "x": 42},
         }
     ) == Bar(baz1=Foo4(1, 2, 42), baz2=Foo2(1, 2))
+    assert decode(
+        {
+            "baz1": {"type": 4, "x1": 1, "x2": 2, "x": 42},
+            "baz2": {"type": 4, "x1": 1, "x2": 2, "x": 42},
+        },
+        _Bar,
+    ) == _Bar(baz1=_Foo4(1, 2, 42), baz2=_Foo2(1, 2))
 
 
 def test_by_field_with_custom_variant_tagger():
     assert VariantWitCustomTaggerOwner.from_dict(
         {"x": {"type": "variantwitcustomtaggersub1"}}
     ) == VariantWitCustomTaggerOwner(VariantWitCustomTaggerSub1())
+    assert decode(
+        {"x": {"type": "_variantwitcustomtaggersub1"}},
+        _VariantWitCustomTaggerOwner,
+    ) == _VariantWitCustomTaggerOwner(_VariantWitCustomTaggerSub1())
+
     assert VariantWitCustomTaggerOwner.from_dict(
         {"x": {"type": "variantwitcustomtaggersub2"}}
     ) == VariantWitCustomTaggerOwner(VariantWitCustomTaggerSub2())
+    assert decode(
+        {"x": {"type": "_variantwitcustomtaggersub2"}},
+        _VariantWitCustomTaggerOwner,
+    ) == _VariantWitCustomTaggerOwner(_VariantWitCustomTaggerSub2())
+
     with pytest.raises(InvalidFieldValue):
         VariantWitCustomTaggerOwner.from_dict({"x": {"type": "unknown"}})
+    with pytest.raises(InvalidFieldValue):
+        decode({"x": {"type": "unknown"}}, _VariantWitCustomTaggerOwner)
diff --git a/tests/test_discriminated_unions/test_parent_via_config.py b/tests/test_discriminated_unions/test_parent_via_config.py
index e5eba15c..6b9dd90e 100644
--- a/tests/test_discriminated_unions/test_parent_via_config.py
+++ b/tests/test_discriminated_unions/test_parent_via_config.py
@@ -6,6 +6,7 @@
 from typing_extensions import Literal
 
 from mashumaro import DataClassDictMixin
+from mashumaro.codecs.basic import decode
 from mashumaro.config import BaseConfig
 from mashumaro.exceptions import (
     InvalidFieldValue,
@@ -27,18 +28,36 @@ class Config(BaseConfig):
         discriminator = Discriminator(include_subtypes=True)
 
 
+@dataclass
+class _VariantBySubtypes:
+    class Config(BaseConfig):
+        discriminator = Discriminator(include_subtypes=True)
+
+
 @dataclass
 class VariantBySubtypesSub1(VariantBySubtypes):
     x: str
     type: Literal[1] = 1
 
 
+@dataclass
+class _VariantBySubtypesSub1(_VariantBySubtypes):
+    x: str
+    type: Literal[1] = 1
+
+
 @dataclass
 class VariantBySubtypesSub2(VariantBySubtypesSub1):
     x: date
     type: Literal[2] = 2
 
 
+@dataclass
+class _VariantBySubtypesSub2(_VariantBySubtypesSub1):
+    x: date
+    type: Literal[2] = 2
+
+
 @dataclass
 class VariantBySubtypesSub3(VariantBySubtypes):
     x: date
@@ -48,29 +67,61 @@ class Config(BaseConfig):
         discriminator = Discriminator(include_subtypes=True)
 
 
+@dataclass
+class _VariantBySubtypesSub3(_VariantBySubtypes):
+    x: date
+    type: Literal[3] = 3
+
+    class Config(BaseConfig):
+        discriminator = Discriminator(include_subtypes=True)
+
+
 @dataclass
 class VariantBySubtypesSub4(VariantBySubtypesSub3):
     pass
 
 
+@dataclass
+class _VariantBySubtypesSub4(_VariantBySubtypesSub3):
+    pass
+
+
 @dataclass
 class VariantByFieldWithSubtypes(DataClassDictMixin):
     class Config(BaseConfig):
         discriminator = Discriminator(field="type", include_subtypes=True)
 
 
+@dataclass
+class _VariantByFieldWithSubtypes:
+    class Config(BaseConfig):
+        discriminator = Discriminator(field="type", include_subtypes=True)
+
+
 @dataclass
 class VariantByFieldWithSubtypesSub1(VariantByFieldWithSubtypes):
     x: Optional[str] = None
     type: Literal[1] = 1
 
 
+@dataclass
+class _VariantByFieldWithSubtypesSub1(_VariantByFieldWithSubtypes):
+    x: Optional[str] = None
+    type: Literal[1] = 1
+
+
 @dataclass
 class VariantByFieldWithSubtypesSub2(VariantByFieldWithSubtypesSub1):
     x: Optional[date] = None
     type: Literal[2] = 2
 
 
+@dataclass
+class _VariantByFieldWithSubtypesSub2(_VariantByFieldWithSubtypesSub1):
+    x: Optional[date] = None
+    type: Literal[2] = 2
+
+
 @dataclass
 class VariantByFieldWithSubtypesSub3(VariantByFieldWithSubtypes):
     x: Optional[date] = None
@@ -80,11 +131,25 @@ class Config(BaseConfig):
         discriminator = Discriminator(field="type", include_subtypes=True)
 
 
+@dataclass
+class _VariantByFieldWithSubtypesSub3(_VariantByFieldWithSubtypes):
+    x: Optional[date] = None
+    type: Literal[3] = 3
+
+    class Config(BaseConfig):
+        discriminator = Discriminator(field="type", include_subtypes=True)
+
+
 @dataclass
 class VariantByFieldWithSubtypesSub4(VariantByFieldWithSubtypesSub3):
     pass
 
 
+@dataclass
+class _VariantByFieldWithSubtypesSub4(_VariantByFieldWithSubtypesSub3):
+    pass
+
+
 @dataclass
 class VariantByFieldWithSupertypesAndSubtypes(DataClassDictMixin):
     type: Literal["unknown"] = "unknown"
@@ -95,6 +160,16 @@ class Config(BaseConfig):
         )
 
 
+@dataclass
+class _VariantByFieldWithSupertypesAndSubtypes:
+    type: Literal["unknown"] = "unknown"
+
+    class Config(BaseConfig):
+        discriminator = Discriminator(
+            field="type", include_supertypes=True, include_subtypes=True
+        )
+
+
 @dataclass
 class VariantByFieldWithSupertypesAndSubtypesSub1(
     VariantByFieldWithSupertypesAndSubtypes
@@ -103,6 +178,14 @@ class VariantByFieldWithSupertypesAndSubtypesSub1(
     type: Literal[1] = 1
 
 
+@dataclass
+class _VariantByFieldWithSupertypesAndSubtypesSub1(
+    _VariantByFieldWithSupertypesAndSubtypes
+):
+    x: Optional[str] = None
+    type: Literal[1] = 1
+
+
 @dataclass
 class VariantByFieldWithSupertypesAndSubtypesSub2(
     VariantByFieldWithSupertypesAndSubtypesSub1
@@ -111,6 +194,14 @@ class VariantByFieldWithSupertypesAndSubtypesSub2(
     type: Literal[2] = 2
 
 
+@dataclass
+class _VariantByFieldWithSupertypesAndSubtypesSub2(
+    _VariantByFieldWithSupertypesAndSubtypesSub1
+):
+    x: Optional[date] = None
+    type: Literal[2] = 2
+
+
 @dataclass
 class VariantByFieldWithSupertypesAndSubtypesSub3(
     VariantByFieldWithSupertypesAndSubtypes
@@ -124,6 +215,19 @@ class Config(BaseConfig):
         )
 
 
+@dataclass
+class _VariantByFieldWithSupertypesAndSubtypesSub3(
+    _VariantByFieldWithSupertypesAndSubtypes
+):
+    x: Optional[date] = None
+    type: Literal[3] = 3
+
+    class Config(BaseConfig):
+        discriminator = Discriminator(
+            field="type", include_supertypes=True, include_subtypes=True
+        )
+
+
 @dataclass
 class VariantByFieldWithSupertypesAndSubtypesSub4(
     VariantByFieldWithSupertypesAndSubtypesSub3
@@ -131,6 +235,13 @@ class VariantByFieldWithSupertypesAndSubtypesSub4(
     pass
 
 
+@dataclass
+class _VariantByFieldWithSupertypesAndSubtypesSub4(
+    _VariantByFieldWithSupertypesAndSubtypesSub3
+):
+    pass
+
+
 @dataclass
 class VariantBySupertypesAndSubtypes(DataClassDictMixin):
     type: Literal["unknown"] = "unknown"
@@ -141,18 +252,40 @@ class Config(BaseConfig):
         )
 
 
+@dataclass
+class _VariantBySupertypesAndSubtypes:
+    type: Literal["unknown"] = "unknown"
+
+    class Config(BaseConfig):
+        discriminator = Discriminator(
+            include_supertypes=True, include_subtypes=True
+        )
+
+
 @dataclass
 class VariantBySupertypesAndSubtypesSub1(VariantBySupertypesAndSubtypes):
     x: Optional[str] = None
     type: Literal[1] = 1
 
 
+@dataclass
+class _VariantBySupertypesAndSubtypesSub1(_VariantBySupertypesAndSubtypes):
+    x: Optional[str] = None
+    type: Literal[1] = 1
+
+
 @dataclass
 class VariantBySupertypesAndSubtypesSub2(VariantBySupertypesAndSubtypesSub1):
     x: Optional[date] = None
     type: Literal[2] = 2
 
 
+@dataclass
+class _VariantBySupertypesAndSubtypesSub2(_VariantBySupertypesAndSubtypesSub1):
+    x: Optional[date] = None
+    type: Literal[2] = 2
+
+
 @dataclass
 class Foo1(DataClassDictMixin):
     x1: int
@@ -161,23 +294,48 @@ class Config(BaseConfig):
         discriminator = Discriminator(field="type", include_subtypes=True)
 
 
+@dataclass
+class _Foo1:
+    x1: int
+
+    class Config(BaseConfig):
+        discriminator = Discriminator(field="type", include_subtypes=True)
+
+
 @dataclass
 class Foo2(Foo1):
     x2: int
 
 
+@dataclass
+class _Foo2(_Foo1):
+    x2: int
+
+
 @dataclass
 class Foo3(Foo2):
     x: int
     type = 3
 
 
+@dataclass
+class _Foo3(_Foo2):
+    x: int
+    type = 3
+
+
 @dataclass
 class Foo4(Foo2):
     x: int
     type = 4
 
 
+@dataclass
+class _Foo4(_Foo2):
+    x: int
+    type = 4
+
+
 @dataclass
 class Bar1(DataClassDictMixin):
     x1: int
@@ -186,23 +344,48 @@ class Config(BaseConfig):
         discriminator = Discriminator(include_subtypes=True)
 
 
+@dataclass
+class _Bar1:
+    x1: int
+
+    class Config(BaseConfig):
+        discriminator = Discriminator(include_subtypes=True)
+
+
 @dataclass
 class Bar2(Bar1):
     x2: int
 
 
+@dataclass
+class _Bar2(_Bar1):
+    x2: int
+
+
 @dataclass
 class Bar3(Bar2):
     x: int
     type = 3
 
 
+@dataclass
+class _Bar3(_Bar2):
+    x: int
+    type = 3
+
+
 @dataclass
 class Bar4(Bar2):
     x: int
     type = 4
 
 
+@dataclass
+class _Bar4(_Bar2):
+    x: int
+    type = 4
+
+
 @dataclass
 class VariantWitCustomTagger(DataClassDictMixin):
     class Config(BaseConfig):
@@ -213,51 +396,96 @@ class Config(BaseConfig):
         )
 
 
+@dataclass
+class _VariantWitCustomTagger:
+    class Config(BaseConfig):
+        discriminator = Discriminator(
+            field="type",
+            include_subtypes=True,
+            variant_tagger_fn=lambda cls: cls.__name__.lower(),
+        )
+
+
 @dataclass
 class VariantWitCustomTaggerSub1(VariantWitCustomTagger):
     pass
 
 
+@dataclass
+class _VariantWitCustomTaggerSub1(_VariantWitCustomTagger):
+    pass
+
+
 @dataclass
 class VariantWitCustomTaggerSub2(VariantWitCustomTagger):
     pass
 
 
+@dataclass
+class _VariantWitCustomTaggerSub2(_VariantWitCustomTagger):
+    pass
+
+
 def test_by_subtypes():
     assert VariantBySubtypes.from_dict(X_1) == VariantBySubtypesSub1(x=DT_STR)
+    assert decode(X_1, _VariantBySubtypes) == _VariantBySubtypesSub1(x=DT_STR)
 
     assert VariantBySubtypes.from_dict(X_2) == VariantBySubtypesSub2(x=DT_DATE)
+    assert decode(X_2, _VariantBySubtypes) == _VariantBySubtypesSub2(x=DT_DATE)
 
     assert VariantBySubtypes.from_dict(X_3) == VariantBySubtypesSub4(DT_DATE)
+    assert decode(X_3, _VariantBySubtypes) == _VariantBySubtypesSub4(DT_DATE)
 
     assert VariantBySubtypesSub3.from_dict(X_3) == VariantBySubtypesSub4(
         DT_DATE
     )
+    assert decode(X_3, _VariantBySubtypesSub3) == _VariantBySubtypesSub4(
+        DT_DATE
+    )
 
     with pytest.raises(SuitableVariantNotFoundError):
         VariantBySubtypesSub3.from_dict(X_1)
+    with pytest.raises(SuitableVariantNotFoundError):
+        decode(X_1, _VariantBySubtypesSub3)
 
     with pytest.raises(SuitableVariantNotFoundError):
         VariantBySubtypesSub3.from_dict(X_2)
+    with pytest.raises(SuitableVariantNotFoundError):
+        decode(X_2, _VariantBySubtypesSub3)
 
     @dataclass
     class MyClass(DataClassDictMixin):
         x: VariantBySubtypes
 
+    @dataclass
+    class _MyClass:
+        x: _VariantBySubtypes
+
     assert MyClass.from_dict({"x": X_1}) == MyClass(
         VariantBySubtypesSub1(DT_STR)
     )
+    assert decode({"x": X_1}, _MyClass) == _MyClass(
+        _VariantBySubtypesSub1(DT_STR)
+    )
 
     assert MyClass.from_dict({"x": X_2}) == MyClass(
         VariantBySubtypesSub2(DT_DATE)
     )
+    assert decode({"x": X_2}, _MyClass) == _MyClass(
+        _VariantBySubtypesSub2(DT_DATE)
+    )
 
     assert MyClass.from_dict({"x": X_3}) == MyClass(
         VariantBySubtypesSub4(DT_DATE)
     )
+    assert decode({"x": X_3}, _MyClass) == _MyClass(
+        _VariantBySubtypesSub4(DT_DATE)
+    )
 
     with pytest.raises(InvalidFieldValue):
         MyClass.from_dict({"x": {}})
+    with pytest.raises(InvalidFieldValue):
+        decode({"x": {}}, _MyClass)
 
 
 def test_by_supertypes():
@@ -277,37 +505,63 @@ def test_by_field_with_subtypes():
     assert VariantByFieldWithSubtypes.from_dict(
         X_1
     ) == VariantByFieldWithSubtypesSub1(x=DT_STR)
+    assert decode(
+        X_1, _VariantByFieldWithSubtypes
+    ) == _VariantByFieldWithSubtypesSub1(x=DT_STR)
 
     assert VariantByFieldWithSubtypes.from_dict(
         X_2
     ) == VariantByFieldWithSubtypesSub2(x=DT_DATE)
+    assert decode(
+        X_2, _VariantByFieldWithSubtypes
+    ) == _VariantByFieldWithSubtypesSub2(x=DT_DATE)
 
     with pytest.raises(SuitableVariantNotFoundError):
         VariantByFieldWithSubtypes.from_dict(X_3)
+    with pytest.raises(SuitableVariantNotFoundError):
+        decode(X_3, _VariantByFieldWithSubtypes)
 
     with pytest.raises(SuitableVariantNotFoundError):
         VariantByFieldWithSubtypesSub3.from_dict(X_3)
+    with pytest.raises(SuitableVariantNotFoundError):
+        decode(X_3, _VariantByFieldWithSubtypesSub3)
 
     with pytest.raises(SuitableVariantNotFoundError):
         VariantByFieldWithSubtypesSub3.from_dict(X_1)
+    with pytest.raises(SuitableVariantNotFoundError):
+        decode(X_1, _VariantByFieldWithSubtypesSub3)
 
     @dataclass
     class MyClass(DataClassDictMixin):
         x: VariantByFieldWithSubtypes
 
+    @dataclass
+    class _MyClass:
+        x: _VariantByFieldWithSubtypes
+
     assert MyClass.from_dict({"x": X_1}) == MyClass(
         VariantByFieldWithSubtypesSub1(DT_STR)
     )
+    assert decode({"x": X_1}, _MyClass) == _MyClass(
+        _VariantByFieldWithSubtypesSub1(DT_STR)
+    )
 
     assert MyClass.from_dict({"x": X_2}) == MyClass(
         VariantByFieldWithSubtypesSub2(DT_DATE)
     )
+    assert decode({"x": X_2}, _MyClass) == _MyClass(
+        _VariantByFieldWithSubtypesSub2(DT_DATE)
+    )
 
     with pytest.raises(InvalidFieldValue):
         MyClass.from_dict({"x": X_3})
+    with pytest.raises(InvalidFieldValue):
+        decode({"x": X_3}, _MyClass)
 
     with pytest.raises(InvalidFieldValue):
         MyClass.from_dict({"x": {}})
+    with pytest.raises(InvalidFieldValue):
+        decode({"x": {}}, _MyClass)
 
 
 def test_by_field_with_supertypes():
@@ -329,82 +583,139 @@ def test_by_field_with_supertypes_and_subtypes():
     assert VariantByFieldWithSupertypesAndSubtypes.from_dict(
         X_1
     ) == VariantByFieldWithSupertypesAndSubtypesSub1(x=DT_STR)
+    assert decode(
+        X_1, _VariantByFieldWithSupertypesAndSubtypes
+    ) == _VariantByFieldWithSupertypesAndSubtypesSub1(x=DT_STR)
 
     assert VariantByFieldWithSupertypesAndSubtypes.from_dict(
         X_2
     ) == VariantByFieldWithSupertypesAndSubtypesSub2(x=DT_DATE)
+    assert decode(
+        X_2, _VariantByFieldWithSupertypesAndSubtypes
+    ) == _VariantByFieldWithSupertypesAndSubtypesSub2(x=DT_DATE)
 
     with pytest.raises(SuitableVariantNotFoundError):
         VariantByFieldWithSupertypesAndSubtypes.from_dict(X_3)
+    with pytest.raises(SuitableVariantNotFoundError):
+        decode(X_3, _VariantByFieldWithSupertypesAndSubtypes)
 
     with pytest.raises(SuitableVariantNotFoundError):
         VariantByFieldWithSupertypesAndSubtypesSub3.from_dict(X_3)
+    with pytest.raises(SuitableVariantNotFoundError):
+        decode(X_3, _VariantByFieldWithSupertypesAndSubtypesSub3)
 
     with pytest.raises(SuitableVariantNotFoundError):
         VariantByFieldWithSupertypesAndSubtypesSub3.from_dict(X_1)
+    with pytest.raises(SuitableVariantNotFoundError):
+        decode(X_1, _VariantByFieldWithSupertypesAndSubtypesSub3)
 
     @dataclass
     class MyClass(DataClassDictMixin):
         x: VariantByFieldWithSupertypesAndSubtypes
 
+    @dataclass
+    class _MyClass:
+        x: _VariantByFieldWithSupertypesAndSubtypes
+
     assert MyClass.from_dict({"x": X_1}) == MyClass(
         VariantByFieldWithSupertypesAndSubtypesSub1(x=DT_STR)
     )
+    assert decode({"x": X_1}, _MyClass) == _MyClass(
+        _VariantByFieldWithSupertypesAndSubtypesSub1(x=DT_STR)
+    )
 
     assert MyClass.from_dict({"x": X_2}) == MyClass(
         VariantByFieldWithSupertypesAndSubtypesSub2(x=DT_DATE)
     )
+    assert decode({"x": X_2}, _MyClass) == _MyClass(
+        _VariantByFieldWithSupertypesAndSubtypesSub2(x=DT_DATE)
+    )
 
     with pytest.raises(InvalidFieldValue):
         MyClass.from_dict({"x": X_3})
+    with pytest.raises(InvalidFieldValue):
+        decode({"x": X_3}, _MyClass)
 
     with pytest.raises(InvalidFieldValue):
         MyClass.from_dict({"x": {"type": "unknown"}})
+    with pytest.raises(InvalidFieldValue):
+        decode({"x": {"type": "unknown"}}, _MyClass)
 
     with pytest.raises(InvalidFieldValue):
         MyClass.from_dict({"x": {}})
+    with pytest.raises(InvalidFieldValue):
+        decode({"x": {}}, _MyClass)
 
     with pytest.raises(InvalidFieldValue):
         MyClass.from_dict({"x": {"type": "invalid"}})
+    with pytest.raises(InvalidFieldValue):
+        decode({"x": {"type": "invalid"}}, _MyClass)
 
 
 def test_by_supertypes_and_subtypes():
     assert VariantBySupertypesAndSubtypes.from_dict(
         X_1
     ) == VariantBySupertypesAndSubtypesSub1(x=DT_STR)
+    assert decode(
+        X_1, _VariantBySupertypesAndSubtypes
+    ) == _VariantBySupertypesAndSubtypesSub1(x=DT_STR)
 
     assert VariantBySupertypesAndSubtypes.from_dict(
         X_2
     ) == VariantBySupertypesAndSubtypesSub2(x=DT_DATE)
+    assert decode(
+        X_2, _VariantBySupertypesAndSubtypes
+    ) == _VariantBySupertypesAndSubtypesSub2(x=DT_DATE)
 
     @dataclass
     class MyClass(DataClassDictMixin):
         x: VariantBySupertypesAndSubtypes
 
+    @dataclass
+    class _MyClass:
+        x: _VariantBySupertypesAndSubtypes
+
     assert MyClass.from_dict({"x": X_1}) == MyClass(
         VariantBySupertypesAndSubtypesSub1(x=DT_STR)
     )
+    assert decode({"x": X_1}, _MyClass) == _MyClass(
+        _VariantBySupertypesAndSubtypesSub1(x=DT_STR)
+    )
 
     assert MyClass.from_dict({"x": X_2}) == MyClass(
         VariantBySupertypesAndSubtypesSub2(x=DT_DATE)
     )
+    assert decode({"x": X_2}, _MyClass) == _MyClass(
+        _VariantBySupertypesAndSubtypesSub2(x=DT_DATE)
+    )
 
     assert MyClass.from_dict({"x": {}}) == MyClass(
         VariantBySupertypesAndSubtypesSub1()
     )
+    assert decode({"x": {}}, _MyClass) == _MyClass(
+        _VariantBySupertypesAndSubtypesSub1()
+    )
 
 
 def test_subclass_tree_with_class_without_field():
     assert Foo1.from_dict({"type": 3, "x1": 1, "x2": 2, "x": 42}) == Foo3(
         1, 2, 42
     )
+    assert decode({"type": 3, "x1": 1, "x2": 2, "x": 42}, _Foo1) == _Foo3(
+        1, 2, 42
+    )
 
     assert Foo1.from_dict({"type": 4, "x1": 1, "x2": 2, "x": 42}) == Foo4(
         1, 2, 42
     )
+    assert decode({"type": 4, "x1": 1, "x2": 2, "x": 42}, _Foo1) == _Foo4(
+        1, 2, 42
+    )
 
     assert Bar1.from_dict({"type": 3, "x1": 1, "x2": 2, "x": 42}) == Bar2(1, 2)
+    assert decode({"type": 3, "x1": 1, "x2": 2, "x": 42}, _Bar1) == _Bar2(1, 2)
     assert Bar1.from_dict({"type": 4, "x1": 1, "x2": 2, "x": 42}) == Bar2(1, 2)
+    assert decode({"type": 4, "x1": 1, "x2": 2, "x": 42}, _Bar1) == _Bar2(1, 2)
 
 
 def test_by_subtypes_with_custom_variant_tagger():
@@ -414,11 +725,27 @@ def test_by_subtypes_with_custom_variant_tagger():
         )
         == VariantWitCustomTaggerSub1()
     )
+    assert (
+        decode(
+            {"type": "_variantwitcustomtaggersub1"}, _VariantWitCustomTagger
+        )
+        == _VariantWitCustomTaggerSub1()
+    )
+
     assert (
         VariantWitCustomTagger.from_dict(
             {"type": "variantwitcustomtaggersub2"}
         )
         == VariantWitCustomTaggerSub2()
     )
+    assert (
+        decode(
+            {"type": "_variantwitcustomtaggersub2"}, _VariantWitCustomTagger
+        )
+        == _VariantWitCustomTaggerSub2()
+    )
+
     with pytest.raises(SuitableVariantNotFoundError):
         VariantWitCustomTagger.from_dict({"type": "unknown"})
+    with pytest.raises(SuitableVariantNotFoundError):
+        decode({"type": "unknown"}, _VariantWitCustomTagger)
diff --git a/tests/test_discriminated_unions/test_union_by_field.py b/tests/test_discriminated_unions/test_union_by_field.py
index deea7943..ad8db133 100644
--- a/tests/test_discriminated_unions/test_union_by_field.py
+++ b/tests/test_discriminated_unions/test_union_by_field.py
@@ -7,6 +7,7 @@
 from typing_extensions import Annotated, Final, Literal
 
 from mashumaro import DataClassDictMixin
+from mashumaro.codecs import BasicDecoder
 from mashumaro.exceptions import InvalidFieldValue
 from mashumaro.types import Discriminator
 
@@ -25,61 +26,61 @@ class VariantType(str, Enum):
 
 
 @dataclass
-class UnannotatedVariantStr(DataClassDictMixin):
+class UnannotatedVariantStr:
     x: str
     type = "str"
 
 
 @dataclass
-class ClassVarVariantStr(DataClassDictMixin):
+class ClassVarVariantStr:
     x: str
     type: ClassVar[str] = "str"
 
 
 @dataclass
-class FinalVariantStr(DataClassDictMixin):
+class FinalVariantStr:
     x: str
     type: Final[str] = "str"
 
 
 @dataclass
-class LiteralVariantStr(DataClassDictMixin):
+class LiteralVariantStr:
     x: str
     type: Literal["str"] = "str"
 
 
 @dataclass
-class EnumVariantStr(DataClassDictMixin):
+class EnumVariantStr:
     x: str
     type: VariantType = VariantType.STR
 
 
 @dataclass
-class UnannotatedVariantDate(DataClassDictMixin):
+class UnannotatedVariantDate:
     x: date
     type = "date"
 
 
 @dataclass
-class ClassVarVariantDate(DataClassDictMixin):
+class ClassVarVariantDate:
     x: date
     type: ClassVar[str] = "date"
 
 
 @dataclass
-class FinalVariantDate(DataClassDictMixin):
+class FinalVariantDate:
     x: date
     type: Final[str] = "date"
 
 
 @dataclass
-class LiteralVariantDate(DataClassDictMixin):
+class LiteralVariantDate:
     x: date
     type: Literal["date"] = "date"
 
 
 @dataclass
-class EnumVariantDate(DataClassDictMixin):
+class EnumVariantDate:
     x: date
     type: VariantType = VariantType.DATE
 
@@ -115,7 +116,7 @@ class EnumVariantDateSubtype(EnumVariantDate):
 
 
 @dataclass
-class ByFieldWithSupertypes(DataClassDictMixin):
+class _ByFieldWithSupertypes:
     unannotated: Annotated[
         Union[UnannotatedVariantStr, UnannotatedVariantDate],
         Discriminator("type", include_supertypes=True),
@@ -139,7 +140,12 @@ class ByFieldWithSupertypes(DataClassDictMixin):
 
 
 @dataclass
-class ByFieldWithSubtypes(DataClassDictMixin):
+class ByFieldWithSupertypes(_ByFieldWithSupertypes, DataClassDictMixin):
+    pass
+
+
+@dataclass
+class _ByFieldWithSubtypes:
     unannotated: Annotated[
         Union[UnannotatedVariantStr, UnannotatedVariantDate],
         Discriminator("type", include_subtypes=True),
@@ -163,7 +169,12 @@ class ByFieldWithSubtypes(DataClassDictMixin):
 
 
 @dataclass
-class BySupertypes(DataClassDictMixin):
+class ByFieldWithSubtypes(_ByFieldWithSubtypes, DataClassDictMixin):
+    pass
+
+
+@dataclass
+class _BySupertypes:
     unannotated: Annotated[
         Union[UnannotatedVariantStr, UnannotatedVariantDate],
         Discriminator(include_supertypes=True),
@@ -187,7 +198,12 @@ class BySupertypes(DataClassDictMixin):
 
 
 @dataclass
-class BySubtypes(DataClassDictMixin):
+class BySupertypes(_BySupertypes, DataClassDictMixin):
+    pass
+
+
+@dataclass
+class _BySubtypes:
     unannotated: Annotated[
         Union[UnannotatedVariantStr, UnannotatedVariantDate],
         Discriminator(include_subtypes=True),
@@ -211,7 +227,12 @@ class BySubtypes(DataClassDictMixin):
 
 
 @dataclass
-class BySupertypesAndSubtypes(DataClassDictMixin):
+class BySubtypes(_BySubtypes, DataClassDictMixin):
+    pass
+
+
+@dataclass
+class _BySupertypesAndSubtypes:
     unannotated: Annotated[
         Union[UnannotatedVariantStr, UnannotatedVariantDate],
         Discriminator(include_supertypes=True, include_subtypes=True),
@@ -235,7 +256,12 @@ class BySupertypesAndSubtypes(DataClassDictMixin):
 
 
 @dataclass
-class ByFieldWithSupertypesAndSubtypes(DataClassDictMixin):
+class BySupertypesAndSubtypes(_BySupertypesAndSubtypes, DataClassDictMixin):
+    pass
+
+
+@dataclass
+class _ByFieldWithSupertypesAndSubtypes:
     unannotated: Annotated[
         Union[UnannotatedVariantStr, UnannotatedVariantDate],
         Discriminator("type", include_supertypes=True, include_subtypes=True),
@@ -259,7 +285,14 @@ class ByFieldWithSupertypesAndSubtypes(DataClassDictMixin):
 
 
 @dataclass
-class ByFieldAndByFieldWithSubtypesInOneField(DataClassDictMixin):
+class ByFieldWithSupertypesAndSubtypes(
+    _ByFieldWithSupertypesAndSubtypes, DataClassDictMixin
+):
+    pass
+
+
+@dataclass
+class _ByFieldAndByFieldWithSubtypesInOneField:
     x: Tuple[
         Annotated[
             Union[UnannotatedVariantStr, UnannotatedVariantDate],
@@ -272,6 +305,13 @@ class ByFieldAndByFieldWithSubtypesInOneField(DataClassDictMixin):
     ]
 
 
+@dataclass
+class ByFieldAndByFieldWithSubtypesInOneField(
+    _ByFieldAndByFieldWithSubtypesInOneField, DataClassDictMixin
+):
+    pass
+
+
 @dataclass
 class VariantWitCustomTagger1:
     pass
@@ -283,7 +323,7 @@ class VariantWitCustomTagger2:
 
 
 @dataclass
-class VariantWitCustomTaggerOwner(DataClassDictMixin):
+class _VariantWitCustomTaggerOwner:
     x: Annotated[
         Union[VariantWitCustomTagger1, VariantWitCustomTagger2],
         Discriminator(
@@ -294,65 +334,21 @@ class VariantWitCustomTaggerOwner(DataClassDictMixin):
     ]
 
 
-def test_by_field_with_supertypes():
-    assert ByFieldWithSupertypes.from_dict(
-        {
-            "unannotated": X_STR,
-            "class_var": X_STR,
-            "literal": X_STR,
-            "final": X_STR,
-            "enum": X_STR,
-        }
-    ) == ByFieldWithSupertypes(
-        unannotated=UnannotatedVariantStr(DT_STR),
-        class_var=ClassVarVariantStr(DT_STR),
-        literal=LiteralVariantStr(DT_STR),
-        final=FinalVariantStr(DT_STR),
-        enum=EnumVariantStr(DT_STR),
-    )
-
-    assert ByFieldWithSupertypes.from_dict(
-        {
-            "unannotated": X_DATE,
-            "class_var": X_DATE,
-            "literal": X_DATE,
-            "final": X_DATE,
-            "enum": X_DATE,
-        }
-    ) == ByFieldWithSupertypes(
-        unannotated=UnannotatedVariantDate(DT_DATE),
-        class_var=ClassVarVariantDate(DT_DATE),
-        literal=LiteralVariantDate(DT_DATE),
-        final=FinalVariantDate(DT_DATE),
-        enum=EnumVariantDate(DT_DATE),
-    )
-
-    with pytest.raises(InvalidFieldValue) as exc_info:
-        ByFieldWithSupertypes.from_dict(
-            {"unannotated": {"x": "2023-05-30", "type": "date_subtype"}}
-        )
-    assert exc_info.value.field_name == "unannotated"
+@dataclass
+class VariantWitCustomTaggerOwner(
+    _VariantWitCustomTaggerOwner, DataClassDictMixin
+):
+    pass
 
 
-def test_by_field_with_subtypes():
-    assert ByFieldWithSubtypes.from_dict(
-        {
-            "unannotated": X_DATE_SUBTYPE,
-            "class_var": X_DATE_SUBTYPE,
-            "literal": X_DATE_SUBTYPE,
-            "final": X_DATE_SUBTYPE,
-            "enum": X_DATE_SUBTYPE,
-        }
-    ) == ByFieldWithSubtypes(
-        unannotated=UnannotatedVariantDateSubtype(DT_DATE),
-        class_var=ClassVarVariantDateSubtype(DT_DATE),
-        literal=LiteralVariantDateSubtype(DT_DATE),
-        final=FinalVariantDateSubtype(DT_DATE),
-        enum=EnumVariantDateSubtype(DT_DATE),
-    )
-
-    with pytest.raises(InvalidFieldValue) as exc_info:
-        ByFieldWithSubtypes.from_dict(
+def test_by_field_with_supertypes():
+    decoder = BasicDecoder(_ByFieldWithSupertypes)
+
+    for func, cls in (
+        (ByFieldWithSupertypes.from_dict, ByFieldWithSupertypes),
+        (decoder.decode, _ByFieldWithSupertypes),
+    ):
+        assert func(
             {
                 "unannotated": X_STR,
                 "class_var": X_STR,
@@ -360,11 +356,19 @@ def test_by_field_with_subtypes():
                 "final": X_STR,
                 "enum": X_STR,
             }
+        ) == cls(
+            unannotated=UnannotatedVariantStr(DT_STR),
+            class_var=ClassVarVariantStr(DT_STR),
+            literal=LiteralVariantStr(DT_STR),
+            final=FinalVariantStr(DT_STR),
+            enum=EnumVariantStr(DT_STR),
         )
-    assert exc_info.value.field_name == "unannotated"
 
-    with pytest.raises(InvalidFieldValue) as exc_info:
-        ByFieldWithSubtypes.from_dict(
+    for func, cls in (
+        (ByFieldWithSupertypes.from_dict, ByFieldWithSupertypes),
+        (decoder.decode, _ByFieldWithSupertypes),
+    ):
+        assert func(
             {
                 "unannotated": X_DATE,
                 "class_var": X_DATE,
@@ -372,97 +376,127 @@ def test_by_field_with_subtypes():
                 "final": X_DATE,
                 "enum": X_DATE,
             }
+        ) == cls(
+            unannotated=UnannotatedVariantDate(DT_DATE),
+            class_var=ClassVarVariantDate(DT_DATE),
+            literal=LiteralVariantDate(DT_DATE),
+            final=FinalVariantDate(DT_DATE),
+            enum=EnumVariantDate(DT_DATE),
         )
-    assert exc_info.value.field_name == "unannotated"
+
+    for func in (ByFieldWithSupertypes.from_dict, decoder.decode):
+        with pytest.raises(InvalidFieldValue) as exc_info:
+            func({"unannotated": {"x": "2023-05-30", "type": "date_subtype"}})
+        assert exc_info.value.field_name == "unannotated"
+
+
+def test_by_field_with_subtypes():
+    decoder = BasicDecoder(_ByFieldWithSubtypes)
+
+    for func, cls in (
+        (ByFieldWithSubtypes.from_dict, ByFieldWithSubtypes),
+        (decoder.decode, _ByFieldWithSubtypes),
+    ):
+        assert func(
+            {
+                "unannotated": X_DATE_SUBTYPE,
+                "class_var": X_DATE_SUBTYPE,
+                "literal": X_DATE_SUBTYPE,
+                "final": X_DATE_SUBTYPE,
+                "enum": X_DATE_SUBTYPE,
+            }
+        ) == cls(
+            unannotated=UnannotatedVariantDateSubtype(DT_DATE),
+            class_var=ClassVarVariantDateSubtype(DT_DATE),
+            literal=LiteralVariantDateSubtype(DT_DATE),
+            final=FinalVariantDateSubtype(DT_DATE),
+            enum=EnumVariantDateSubtype(DT_DATE),
+        )
+
+    for func in (ByFieldWithSubtypes.from_dict, decoder.decode):
+        with pytest.raises(InvalidFieldValue) as exc_info:
+            func(
+                {
+                    "unannotated": X_STR,
+                    "class_var": X_STR,
+                    "literal": X_STR,
+                    "final": X_STR,
+                    "enum": X_STR,
+                }
+            )
+        assert exc_info.value.field_name == "unannotated"
+
+    for func in (ByFieldWithSubtypes.from_dict, decoder.decode):
+        with pytest.raises(InvalidFieldValue) as exc_info:
+            func(
+                {
+                    "unannotated": X_DATE,
+                    "class_var": X_DATE,
+                    "literal": X_DATE,
+                    "final": X_DATE,
+                    "enum": X_DATE,
+                }
+            )
+        assert exc_info.value.field_name == "unannotated"
 
 
 def test_by_field_with_supertypes_and_subtypes():
-    assert ByFieldWithSupertypesAndSubtypes.from_dict(
-        {
-            "unannotated": X_STR,
-            "class_var": X_STR,
-            "literal": X_STR,
-            "final": X_STR,
-            "enum": X_STR,
-        }
-    ) == ByFieldWithSupertypesAndSubtypes(
-        unannotated=UnannotatedVariantStr(DT_STR),
-        class_var=ClassVarVariantStr(DT_STR),
-        literal=LiteralVariantStr(DT_STR),
-        final=FinalVariantStr(DT_STR),
-        enum=EnumVariantStr(DT_STR),
-    )
-
-    assert ByFieldWithSupertypesAndSubtypes.from_dict(
-        {
-            "unannotated": X_DATE,
-            "class_var": X_DATE,
-            "literal": X_DATE,
-            "final": X_DATE,
-            "enum": X_DATE,
-        }
-    ) == ByFieldWithSupertypesAndSubtypes(
-        unannotated=UnannotatedVariantDate(DT_DATE),
-        class_var=ClassVarVariantDate(DT_DATE),
-        literal=LiteralVariantDate(DT_DATE),
-        final=FinalVariantDate(DT_DATE),
-        enum=EnumVariantDate(DT_DATE),
-    )
-
-    assert ByFieldWithSupertypesAndSubtypes.from_dict(
-        {
-            "unannotated": X_DATE_SUBTYPE,
-            "class_var": X_DATE_SUBTYPE,
-            "literal": X_DATE_SUBTYPE,
-            "final": X_DATE_SUBTYPE,
-            "enum": X_DATE_SUBTYPE,
-        }
-    ) == ByFieldWithSupertypesAndSubtypes(
-        unannotated=UnannotatedVariantDateSubtype(DT_DATE),
-        class_var=ClassVarVariantDateSubtype(DT_DATE),
-        literal=LiteralVariantDateSubtype(DT_DATE),
-        final=FinalVariantDateSubtype(DT_DATE),
-        enum=EnumVariantDateSubtype(DT_DATE),
-    )
+    decoder = BasicDecoder(_ByFieldWithSupertypesAndSubtypes)
 
+    for func, cls in (
+        (
+            ByFieldWithSupertypesAndSubtypes.from_dict,
+            ByFieldWithSupertypesAndSubtypes,
+        ),
+        (decoder.decode, _ByFieldWithSupertypesAndSubtypes),
+    ):
+        assert func(
+            {
+                "unannotated": X_STR,
+                "class_var": X_STR,
+                "literal": X_STR,
+                "final": X_STR,
+                "enum": X_STR,
+            }
+        ) == cls(
+            unannotated=UnannotatedVariantStr(DT_STR),
+            class_var=ClassVarVariantStr(DT_STR),
+            literal=LiteralVariantStr(DT_STR),
+            final=FinalVariantStr(DT_STR),
+            enum=EnumVariantStr(DT_STR),
+        )
 
-def test_by_supertypes():
-    assert BySupertypes.from_dict(
-        {
-            "unannotated": X_STR,
-            "class_var": X_STR,
-            "literal": X_STR,
-            "final": X_STR,
-            "enum": X_STR,
-        }
-    ) == BySupertypes(
-        unannotated=UnannotatedVariantStr(DT_STR),
-        class_var=ClassVarVariantStr(DT_STR),
-        literal=LiteralVariantStr(DT_STR),
-        final=FinalVariantStr(DT_STR),
-        enum=EnumVariantStr(DT_STR),
-    )
-
-    assert BySupertypes.from_dict(
-        {
-            "unannotated": X_DATE,
-            "class_var": X_DATE,
-            "literal": X_DATE,
-            "final": X_DATE,
-            "enum": X_DATE,
-        }
-    ) == BySupertypes(
-        unannotated=UnannotatedVariantStr(DT_STR),
-        class_var=ClassVarVariantStr(DT_STR),
-        literal=LiteralVariantDate(DT_DATE),
-        # using final without field discriminator can lead to unexpected result
-        final=FinalVariantStr(DT_STR, type=VariantType.DATE),
-        # using enum without field discriminator can lead to unexpected result
-        enum=EnumVariantStr(DT_STR, type=VariantType.DATE),
-    )
-
-    with pytest.raises(InvalidFieldValue) as exc_info:
-        BySupertypes.from_dict(
+    for func, cls in (
+        (
+            ByFieldWithSupertypesAndSubtypes.from_dict,
+            ByFieldWithSupertypesAndSubtypes,
+        ),
+        (decoder.decode, _ByFieldWithSupertypesAndSubtypes),
+    ):
+        assert func(
+            {
+                "unannotated": X_DATE,
+                "class_var": X_DATE,
+                "literal": X_DATE,
+                "final": X_DATE,
+                "enum": X_DATE,
+            }
+        ) == cls(
+            unannotated=UnannotatedVariantDate(DT_DATE),
+            class_var=ClassVarVariantDate(DT_DATE),
+            literal=LiteralVariantDate(DT_DATE),
+            final=FinalVariantDate(DT_DATE),
+            enum=EnumVariantDate(DT_DATE),
+        )
+
+    for func, cls in (
+        (
+            ByFieldWithSupertypesAndSubtypes.from_dict,
+            ByFieldWithSupertypesAndSubtypes,
+        ),
+        (decoder.decode, _ByFieldWithSupertypesAndSubtypes),
+    ):
+        assert func(
             {
                 "unannotated": X_DATE_SUBTYPE,
                 "class_var": X_DATE_SUBTYPE,
@@ -470,47 +504,23 @@ def test_by_supertypes():
                 "final": X_DATE_SUBTYPE,
                 "enum": X_DATE_SUBTYPE,
             }
+        ) == cls(
+            unannotated=UnannotatedVariantDateSubtype(DT_DATE),
+            class_var=ClassVarVariantDateSubtype(DT_DATE),
+            literal=LiteralVariantDateSubtype(DT_DATE),
+            final=FinalVariantDateSubtype(DT_DATE),
+            enum=EnumVariantDateSubtype(DT_DATE),
         )
-    assert exc_info.value.field_name == "literal"
 
 
-def test_by_subtypes():
-    assert BySubtypes.from_dict(
-        {
-            "unannotated": X_DATE_SUBTYPE,
-            "class_var": X_DATE_SUBTYPE,
-            "literal": X_DATE_SUBTYPE,
-            "final": X_DATE_SUBTYPE,
-            "enum": X_DATE_SUBTYPE,
-        }
-    ) == BySubtypes(
-        unannotated=UnannotatedVariantDateSubtype(DT_DATE),
-        class_var=ClassVarVariantDateSubtype(DT_DATE),
-        literal=LiteralVariantDateSubtype(DT_DATE),
-        final=FinalVariantDateSubtype(DT_DATE),
-        enum=EnumVariantDateSubtype(DT_DATE),
-    )
-
-    assert BySubtypes.from_dict(
-        {
-            "unannotated": X_DATE,
-            "class_var": X_DATE,
-            "literal": X_DATE_SUBTYPE,
-            "final": X_DATE,
-            "enum": X_DATE,
-        }
-    ) == BySubtypes(
-        unannotated=UnannotatedVariantDateSubtype(DT_DATE),
-        class_var=ClassVarVariantDateSubtype(DT_DATE),
-        literal=LiteralVariantDateSubtype(DT_DATE),
-        # using final without field discriminator can lead to unexpected result
-        final=FinalVariantDateSubtype(DT_DATE, type=VariantType.DATE),
-        # using enum without field discriminator can lead to unexpected result
-        enum=EnumVariantDateSubtype(DT_DATE, type=VariantType.DATE),
-    )
-
-    with pytest.raises(InvalidFieldValue) as exc_info:
-        BySubtypes.from_dict(
+def test_by_supertypes():
+    decoder = BasicDecoder(_BySupertypes)
+
+    for func, cls in (
+        (BySupertypes.from_dict, BySupertypes),
+        (decoder.decode, _BySupertypes),
+    ):
+        assert func(
             {
                 "unannotated": X_STR,
                 "class_var": X_STR,
@@ -518,91 +528,223 @@ def test_by_subtypes():
                 "final": X_STR,
                 "enum": X_STR,
             }
+        ) == cls(
+            unannotated=UnannotatedVariantStr(DT_STR),
+            class_var=ClassVarVariantStr(DT_STR),
+            literal=LiteralVariantStr(DT_STR),
+            final=FinalVariantStr(DT_STR),
+            enum=EnumVariantStr(DT_STR),
+        )
+
+    for func, cls in (
+        (BySupertypes.from_dict, BySupertypes),
+        (decoder.decode, _BySupertypes),
+    ):
+        assert func(
+            {
+                "unannotated": X_DATE,
+                "class_var": X_DATE,
+                "literal": X_DATE,
+                "final": X_DATE,
+                "enum": X_DATE,
+            }
+        ) == cls(
+            unannotated=UnannotatedVariantStr(DT_STR),
+            class_var=ClassVarVariantStr(DT_STR),
+            literal=LiteralVariantDate(DT_DATE),
+            # final without field discriminator can lead to unexpected result
+            final=FinalVariantStr(DT_STR, type=VariantType.DATE),
+            # enum without field discriminator can lead to unexpected result
+            enum=EnumVariantStr(DT_STR, type=VariantType.DATE),
+        )
+
+    for func in (BySupertypes.from_dict, decoder.decode):
+        with pytest.raises(InvalidFieldValue) as exc_info:
+            func(
+                {
+                    "unannotated": X_DATE_SUBTYPE,
+                    "class_var": X_DATE_SUBTYPE,
+                    "literal": X_DATE_SUBTYPE,
+                    "final": X_DATE_SUBTYPE,
+                    "enum": X_DATE_SUBTYPE,
+                }
+            )
+        assert exc_info.value.field_name == "literal"
+
+
+def test_by_subtypes():
+    decoder = BasicDecoder(_BySubtypes)
+
+    for func, cls in (
+        (BySubtypes.from_dict, BySubtypes),
+        (decoder.decode, _BySubtypes),
+    ):
+        assert func(
+            {
+                "unannotated": X_DATE_SUBTYPE,
+                "class_var": X_DATE_SUBTYPE,
+                "literal": X_DATE_SUBTYPE,
+                "final": X_DATE_SUBTYPE,
+                "enum": X_DATE_SUBTYPE,
+            }
+        ) == cls(
+            unannotated=UnannotatedVariantDateSubtype(DT_DATE),
+            class_var=ClassVarVariantDateSubtype(DT_DATE),
+            literal=LiteralVariantDateSubtype(DT_DATE),
+            final=FinalVariantDateSubtype(DT_DATE),
+            enum=EnumVariantDateSubtype(DT_DATE),
+        )
+
+    for func, cls in (
+        (BySubtypes.from_dict, BySubtypes),
+        (decoder.decode, _BySubtypes),
+    ):
+        assert func(
+            {
+                "unannotated": X_DATE,
+                "class_var": X_DATE,
+                "literal": X_DATE_SUBTYPE,
+                "final": X_DATE,
+                "enum": X_DATE,
+            }
+        ) == cls(
+            unannotated=UnannotatedVariantDateSubtype(DT_DATE),
+            class_var=ClassVarVariantDateSubtype(DT_DATE),
+            literal=LiteralVariantDateSubtype(DT_DATE),
+            # final without field discriminator can lead to unexpected result
+            final=FinalVariantDateSubtype(DT_DATE, type=VariantType.DATE),
+            # enum without field discriminator can lead to unexpected result
+            enum=EnumVariantDateSubtype(DT_DATE, type=VariantType.DATE),
         )
-    assert exc_info.value.field_name == "literal"
+
+    for func in (BySubtypes.from_dict, decoder.decode):
+        with pytest.raises(InvalidFieldValue) as exc_info:
+            func(
+                {
+                    "unannotated": X_STR,
+                    "class_var": X_STR,
+                    "literal": X_STR,
+                    "final": X_STR,
+                    "enum": X_STR,
+                }
+            )
+        assert exc_info.value.field_name == "literal"
 
 
 def test_by_supertypes_and_subtypes():
-    assert BySupertypesAndSubtypes.from_dict(
-        {
-            "unannotated": X_DATE_SUBTYPE,
-            "class_var": X_DATE_SUBTYPE,
-            "literal": X_DATE_SUBTYPE,
-            "final": X_DATE_SUBTYPE,
-            "enum": X_DATE_SUBTYPE,
-        }
-    ) == BySupertypesAndSubtypes(
-        unannotated=UnannotatedVariantDateSubtype(DT_DATE),
-        class_var=ClassVarVariantDateSubtype(DT_DATE),
-        literal=LiteralVariantDateSubtype(DT_DATE),
-        final=FinalVariantDateSubtype(DT_DATE),
-        enum=EnumVariantDateSubtype(DT_DATE),
-    )
-
-    assert BySupertypesAndSubtypes.from_dict(
-        {
-            "unannotated": X_STR,
-            "class_var": X_STR,
-            "literal": X_STR,
-            "final": X_STR,
-            "enum": X_STR,
-        }
-    ) == BySupertypesAndSubtypes(
-        unannotated=UnannotatedVariantDateSubtype(DT_DATE),
-        class_var=ClassVarVariantDateSubtype(DT_DATE),
-        literal=LiteralVariantStr(DT_STR),
-        # using final without field discriminator can lead to unexpected result
-        final=FinalVariantDateSubtype(DT_DATE, type=VariantType.STR),
-        # using enum without field discriminator can lead to unexpected result
-        enum=EnumVariantDateSubtype(DT_DATE, type=VariantType.STR),
-    )
-
-    assert BySupertypesAndSubtypes.from_dict(
-        {
-            "unannotated": X_DATE,
-            "class_var": X_DATE,
-            "literal": X_DATE,
-            "final": X_DATE,
-            "enum": X_DATE,
-        }
-    ) == BySupertypesAndSubtypes(
-        unannotated=UnannotatedVariantDateSubtype(DT_DATE),
-        class_var=ClassVarVariantDateSubtype(DT_DATE),
-        literal=LiteralVariantDate(DT_DATE),
-        # using final without field discriminator can lead to unexpected result
-        final=FinalVariantDateSubtype(DT_DATE, type=VariantType.DATE),
-        # using enum without field discriminator can lead to unexpected result
-        enum=EnumVariantDateSubtype(DT_DATE, type=VariantType.DATE),
-    )
+    decoder = BasicDecoder(_BySupertypesAndSubtypes)
+
+    for func, cls in (
+        (BySupertypesAndSubtypes.from_dict, BySupertypesAndSubtypes),
+        (decoder.decode, _BySupertypesAndSubtypes),
+    ):
+        assert func(
+            {
+                "unannotated": X_DATE_SUBTYPE,
+                "class_var": X_DATE_SUBTYPE,
+                "literal": X_DATE_SUBTYPE,
+                "final": X_DATE_SUBTYPE,
+                "enum": X_DATE_SUBTYPE,
+            }
+        ) == cls(
+            unannotated=UnannotatedVariantDateSubtype(DT_DATE),
+            class_var=ClassVarVariantDateSubtype(DT_DATE),
+            literal=LiteralVariantDateSubtype(DT_DATE),
+            final=FinalVariantDateSubtype(DT_DATE),
+            enum=EnumVariantDateSubtype(DT_DATE),
+        )
+
+    for func, cls in (
+        (BySupertypesAndSubtypes.from_dict, BySupertypesAndSubtypes),
+        (decoder.decode, _BySupertypesAndSubtypes),
+    ):
+        assert func(
+            {
+                "unannotated": X_STR,
+                "class_var": X_STR,
+                "literal": X_STR,
+                "final": X_STR,
+                "enum": X_STR,
+            }
+        ) == cls(
+            unannotated=UnannotatedVariantDateSubtype(DT_DATE),
+            class_var=ClassVarVariantDateSubtype(DT_DATE),
+            literal=LiteralVariantStr(DT_STR),
+            # using final without field discriminator
+            # can lead to unexpected result
+            final=FinalVariantDateSubtype(DT_DATE, type=VariantType.STR),
+            # using enum without field discriminator
+            # can lead to unexpected result
+            enum=EnumVariantDateSubtype(DT_DATE, type=VariantType.STR),
+        )
+
+    for func, cls in (
+        (BySupertypesAndSubtypes.from_dict, BySupertypesAndSubtypes),
+        (decoder.decode, _BySupertypesAndSubtypes),
+    ):
+        assert func(
+            {
+                "unannotated": X_DATE,
+                "class_var": X_DATE,
+                "literal": X_DATE,
+                "final": X_DATE,
+                "enum": X_DATE,
+            }
+        ) == cls(
+            unannotated=UnannotatedVariantDateSubtype(DT_DATE),
+            class_var=ClassVarVariantDateSubtype(DT_DATE),
+            literal=LiteralVariantDate(DT_DATE),
+            # final without field discriminator can lead to unexpected result
+            final=FinalVariantDateSubtype(DT_DATE, type=VariantType.DATE),
+            # enum without field discriminator can lead to unexpected result
+            enum=EnumVariantDateSubtype(DT_DATE, type=VariantType.DATE),
+        )
 
 
 def test_tuple_with_discriminated_elements():
-    assert ByFieldAndByFieldWithSubtypesInOneField.from_dict(
-        {"x": [X_STR, X_DATE_SUBTYPE]}
-    ) == ByFieldAndByFieldWithSubtypesInOneField(
+    decoder = BasicDecoder(_ByFieldAndByFieldWithSubtypesInOneField)
+
+    for func, cls in (
         (
-            UnannotatedVariantStr(DT_STR),
-            UnannotatedVariantDateSubtype(DT_DATE),
+            ByFieldAndByFieldWithSubtypesInOneField.from_dict,
+            ByFieldAndByFieldWithSubtypesInOneField,
         ),
-    )
-
-    with pytest.raises(InvalidFieldValue):
-        ByFieldAndByFieldWithSubtypesInOneField.from_dict(
-            {"x": [X_DATE_SUBTYPE, X_DATE_SUBTYPE]}
+        (decoder.decode, _ByFieldAndByFieldWithSubtypesInOneField),
+    ):
+        assert func({"x": [X_STR, X_DATE_SUBTYPE]}) == cls(
+            (
+                UnannotatedVariantStr(DT_STR),
+                UnannotatedVariantDateSubtype(DT_DATE),
+            ),
         )
 
-    with pytest.raises(InvalidFieldValue):
-        ByFieldAndByFieldWithSubtypesInOneField.from_dict(
-            {"x": [X_STR, X_STR]}
-        )
+    for func in (
+        ByFieldAndByFieldWithSubtypesInOneField.from_dict,
+        decoder.decode,
+    ):
+        with pytest.raises(InvalidFieldValue):
+            func({"x": [X_DATE_SUBTYPE, X_DATE_SUBTYPE]})
+
+    for func in (
+        ByFieldAndByFieldWithSubtypesInOneField.from_dict,
+        decoder.decode,
+    ):
+        with pytest.raises(InvalidFieldValue):
+            func({"x": [X_STR, X_STR]})
 
 
 def test_by_field_with_subtypes_with_custom_variant_tagger():
-    assert VariantWitCustomTaggerOwner.from_dict(
-        {"x": {"type": "variantwitcustomtagger1"}}
-    ) == VariantWitCustomTaggerOwner(VariantWitCustomTagger1())
-    assert VariantWitCustomTaggerOwner.from_dict(
-        {"x": {"type": "variantwitcustomtagger2"}}
-    ) == VariantWitCustomTaggerOwner(VariantWitCustomTagger2())
-    with pytest.raises(InvalidFieldValue):
-        VariantWitCustomTaggerOwner.from_dict({"x": {"type": "unknown"}})
+    decoder = BasicDecoder(_VariantWitCustomTaggerOwner)
+
+    for func, cls in (
+        (VariantWitCustomTaggerOwner.from_dict, VariantWitCustomTaggerOwner),
+        (decoder.decode, _VariantWitCustomTaggerOwner),
+    ):
+        assert func({"x": {"type": "variantwitcustomtagger1"}}) == cls(
+            VariantWitCustomTagger1()
+        )
+        assert func({"x": {"type": "variantwitcustomtagger2"}}) == cls(
+            VariantWitCustomTagger2()
+        )
+        with pytest.raises(InvalidFieldValue):
+            func({"x": {"type": "unknown"}})
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index 3e6c6b5f..a87030ff 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -1,5 +1,11 @@
+from dataclasses import dataclass
 from typing import List, Union
 
+import pytest
+
+from mashumaro import DataClassDictMixin
+from mashumaro.codecs import BasicDecoder
+from mashumaro.core.meta.helpers import type_name
 from mashumaro.exceptions import (
     InvalidFieldValue,
     MissingDiscriminatorError,
@@ -163,3 +169,24 @@ def test_suitable_variant_not_found_error():
     assert exc.variants_type == Union[str, int]
     assert exc.discriminator_name == "type"
     assert str(exc) == "typing.Union[str, int] has no suitable subtype"
+
+
+def test_deserialize_dataclass_from_wrong_value_type():
+    @dataclass
+    class MyClass(DataClassDictMixin):
+        x: str
+
+    with pytest.raises(ValueError) as exc_info:
+        MyClass.from_dict(42)
+    assert str(exc_info.value) == (
+        f"Argument for {type_name(MyClass)}."
+        f"__mashumaro_from_dict__ method should be a dict instance"
+    )
+
+    decoder = BasicDecoder(MyClass)
+    with pytest.raises(ValueError) as exc_info:
+        decoder.decode(42)
+    assert str(exc_info.value) == (
+        f"Argument for {type_name(MyClass)}."
+        f"__mashumaro_from_dict__ method should be a dict instance"
+    )
diff --git a/tests/test_helper.py b/tests/test_helper.py
index b1af2859..2a141855 100644
--- a/tests/test_helper.py
+++ b/tests/test_helper.py
@@ -17,12 +17,12 @@ def test_field_options_helper():
     }
 
     def serialize(x):
-        return x  # pragma no cover
+        return x  # pragma: no cover
 
     def deserialize(x):
-        return x  # pragma no cover
+        return x  # pragma: no cover
 
-    class TestSerializationStrategy(SerializationStrategy):  # pragma no cover
+    class TestSerializationStrategy(SerializationStrategy):  # pragma: no cover
         def deserialize(self, value):
             return value
 
diff --git a/tests/test_hooks.py b/tests/test_hooks.py
index a1ae36c5..06164dc7 100644
--- a/tests/test_hooks.py
+++ b/tests/test_hooks.py
@@ -63,7 +63,7 @@ class DataClass(DataClassDictMixin):
 
             @no_type_check
             def __pre_deserialize__(self, d: Dict[Any, Any]) -> Dict[Any, Any]:
-                pass  # pragma no cover
+                pass  # pragma: no cover
 
 
 def test_bad_post_deserialize_hook():
@@ -74,7 +74,7 @@ class DataClass(DataClassDictMixin):
 
             @no_type_check
             def __post_deserialize__(self, obj: "DataClass") -> "DataClass":
-                pass  # pragma no cover
+                pass  # pragma: no cover
 
 
 def test_pre_deserialize_hook():
@@ -161,17 +161,17 @@ def test_hook_in_parent_class(mocker):
     class A:
         @classmethod
         def __pre_deserialize__(cls, d):
-            return d  # pragma no cover
+            return d  # pragma: no cover
 
         @classmethod
         def __post_deserialize__(cls, obj):
-            return obj  # pragma no cover
+            return obj  # pragma: no cover
 
         def __pre_serialize__(self):
-            return self  # pragma no cover
+            return self  # pragma: no cover
 
         def __post_serialize__(self, d):
-            return d  # pragma no cover
+            return d  # pragma: no cover
 
     @dataclass
     class B(A, DataClassDictMixin):
@@ -210,7 +210,7 @@ def test_post_deserialize_hook_with_empty_dataclass():
     class MyClass(DataClassDictMixin):
         @classmethod
         def __post_deserialize__(cls, obj):
-            return obj  # pragma no cover
+            return obj  # pragma: no cover
 
     assert MyClass.from_dict({}) == MyClass()
 
diff --git a/tests/test_jsonschema/test_json_schema_common.py b/tests/test_jsonschema/test_json_schema_common.py
new file mode 100644
index 00000000..9b39e1c3
--- /dev/null
+++ b/tests/test_jsonschema/test_json_schema_common.py
@@ -0,0 +1,11 @@
+from mashumaro.config import BaseConfig
+from mashumaro.jsonschema.schema import Instance
+
+
+def test_instance_get_configs():
+    instance = Instance(int)
+    assert instance.get_owner_config() is BaseConfig
+    assert instance.get_self_config() is BaseConfig
+
+    derived = instance.derive()
+    assert derived.get_self_config() is instance.get_self_config()
diff --git a/tests/test_jsonschema/test_jsonschema_generation.py b/tests/test_jsonschema/test_jsonschema_generation.py
index 964bdd69..0456003d 100644
--- a/tests/test_jsonschema/test_jsonschema_generation.py
+++ b/tests/test_jsonschema/test_jsonschema_generation.py
@@ -114,7 +114,7 @@
 
 
 def dummy_serialize_as_str(_: Any) -> str:
-    return "dummy"  # pragma no cover
+    return "dummy"  # pragma: no cover
 
 
 class ThirdPartyType:
@@ -515,14 +515,16 @@ class Config:
 
 
 def test_jsonschema_for_named_tuple_with_overridden_serialization_method():
-    class MySerializationStrategy(SerializationStrategy):  # pragma no cover
+    class MySerializationStrategy(SerializationStrategy):  # pragma: no cover
         def serialize(self, value: Any) -> MyNamedTuple:
             pass
 
         def deserialize(self, value: Any) -> Any:
             pass
 
-    class MyAnySerializationStrategy(SerializationStrategy):  # pragma no cover
+    class MyAnySerializationStrategy(
+        SerializationStrategy
+    ):  # pragma: no cover
         def serialize(self, value: Any) -> Any:
             pass
 
@@ -948,7 +950,7 @@ class Config(BaseConfig):
 
 
 def test_overridden_serialization_method_without_return_annotation():
-    def as_timestamp(dt: datetime.datetime):  # pragma no cover
+    def as_timestamp(dt: datetime.datetime):  # pragma: no cover
         return dt.timestamp()
 
     @dataclass
@@ -967,12 +969,12 @@ class Config(BaseConfig):
 
 def test_overridden_serialization_method_with_return_annotation():
     def as_timestamp(dt: datetime.datetime) -> float:
-        return dt.timestamp()  # pragma no cover
+        return dt.timestamp()  # pragma: no cover
 
     def first_datetime_as_timestamp(
         seq: List[datetime.datetime],
     ) -> float:
-        return as_timestamp(seq[0])  # pragma no cover
+        return as_timestamp(seq[0])  # pragma: no cover
 
     @dataclass
     class DataClass:
@@ -1051,7 +1053,7 @@ class Config(BaseConfig):
 
 def test_dataclass_overridden_serialization_method():
     def serialize_as_str(value: Any) -> str:
-        return str(value)  # pragma no cover
+        return str(value)  # pragma: no cover
 
     @dataclass
     class Inner:
diff --git a/tests/test_meta.py b/tests/test_meta.py
index 353be2d0..1c1ec3e0 100644
--- a/tests/test_meta.py
+++ b/tests/test_meta.py
@@ -117,18 +117,18 @@ class DataClass(DataClassDictMixin):
 def test_get_class_that_defines_method():
     class A:
         def foo(self):
-            pass  # pragma no cover
+            pass  # pragma: no cover
 
         @classmethod
         def bar(cls):
-            pass  # pragma no cover
+            pass  # pragma: no cover
 
         def foobar(self):
-            pass  # pragma no cover
+            pass  # pragma: no cover
 
     class B(A):
         def foobar(self):
-            pass  # pragma no cover
+            pass  # pragma: no cover
 
     assert get_class_that_defines_method("foo", B) == A
     assert get_class_that_defines_method("bar", B) == A
@@ -717,7 +717,7 @@ def test_ensure_mapping_key_type_hashable():
 
 def test_get_function_arg_annotation():
     def foo(x: int, y: Dialect) -> None:
-        pass  # pragma no cover
+        pass  # pragma: no cover
 
     assert get_function_arg_annotation(foo, "x") == int
     assert get_function_arg_annotation(foo, "y") == Dialect
@@ -731,7 +731,7 @@ def foo(x: int, y: Dialect) -> None:
 
 def test_get_function_return_annotation():
     def foo(x: int, y: Dialect) -> Dialect:
-        pass  # pragma no cover
+        pass  # pragma: no cover
 
     assert get_function_return_annotation(foo) == Dialect
 
diff --git a/tests/test_self.py b/tests/test_self.py
index 0b4f8ad0..90380ee1 100644
--- a/tests/test_self.py
+++ b/tests/test_self.py
@@ -5,6 +5,8 @@
 from typing_extensions import Self
 
 from mashumaro import DataClassDictMixin
+from mashumaro.codecs import BasicDecoder, BasicEncoder
+from mashumaro.codecs.orjson import ORJSONDecoder, ORJSONEncoder
 from mashumaro.mixins.orjson import DataClassORJSONMixin
 
 
@@ -18,6 +20,11 @@ class DataClassDictChild(DataClassDict):
     x: int = 42
 
 
+@dataclass
+class DataClassDictWithoutMixin:
+    next: Optional[Self] = None
+
+
 @dataclass
 class DataClassOrjson(DataClassORJSONMixin):
     next: Optional[Self] = None
@@ -37,6 +44,17 @@ def test_dataclass_dict_with_self():
     assert DataClassDict.from_dict({}) == DataClassDict()
 
 
+def test_dataclass_dict_with_self_without_mixin():
+    decoder = BasicDecoder(DataClassDictWithoutMixin)
+    encoder = BasicEncoder(DataClassDictWithoutMixin)
+    obj = DataClassDictWithoutMixin(DataClassDictWithoutMixin())
+    assert encoder.encode(obj) == {"next": {"next": None}}
+    assert decoder.decode({"next": {"next": None}}) == obj
+    assert encoder.encode(DataClassDictWithoutMixin()) == {"next": None}
+    assert decoder.decode({"next": None}) == DataClassDictWithoutMixin()
+    assert decoder.decode({}) == DataClassDictWithoutMixin()
+
+
 def test_dataclass_dict_child_with_self():
     obj = DataClassDictChild(DataClassDictChild())
     assert obj.to_dict() == {"x": 42, "next": {"x": 42, "next": None}}
@@ -63,6 +81,19 @@ def test_dataclass_orjson_with_self():
     assert DataClassOrjson.from_json(b"{}") == DataClassOrjson()
 
 
+def test_dataclass_orjson_with_self_without_mixin():
+    decoder = ORJSONDecoder(DataClassDictWithoutMixin)
+    encoder = ORJSONEncoder(DataClassDictWithoutMixin)
+    obj = DataClassDictWithoutMixin(DataClassDictWithoutMixin())
+    dump = orjson.dumps({"next": {"next": None}})
+    assert encoder.encode(obj) == dump
+    assert decoder.decode(dump) == obj
+    dump = orjson.dumps({"next": None})
+    assert encoder.encode(DataClassDictWithoutMixin()) == dump
+    assert decoder.decode(dump) == DataClassDictWithoutMixin()
+    assert decoder.decode(b"{}") == DataClassDictWithoutMixin()
+
+
 def test_dataclass_orjson_child_with_self():
     obj = DataClassOrjsonChild(DataClassOrjsonChild())
     assert obj.to_dict() == {"x": 42, "next": {"x": 42, "next": None}}
diff --git a/tests/test_types.py b/tests/test_types.py
index f7f15ba9..bdad3e36 100644
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -42,28 +42,28 @@ def _deserialize(cls, value: date):
 class MyAnnotatedSerializableTypeWithoutAnnotationsInDeserialize(
     SerializableType, use_annotations=True
 ):
-    def __init__(self, value):  # pragma no cover
+    def __init__(self, value):  # pragma: no cover
         self.value = value
 
-    def _serialize(self) -> date:  # pragma no cover
+    def _serialize(self) -> date:  # pragma: no cover
         return self.value
 
     @classmethod
-    def _deserialize(cls, value):  # pragma no cover
+    def _deserialize(cls, value):  # pragma: no cover
         return cls(value)
 
 
 class MyAnnotatedSerializableTypeWithoutAnnotationsInSerialize(
     SerializableType, use_annotations=True
 ):
-    def __init__(self, value):  # pragma no cover
+    def __init__(self, value):  # pragma: no cover
         self.value = value
 
-    def _serialize(self):  # pragma no cover
+    def _serialize(self):  # pragma: no cover
         return self.value
 
     @classmethod
-    def _deserialize(cls, value: date):  # pragma no cover
+    def _deserialize(cls, value: date):  # pragma: no cover
         return cls(value)
 
 
@@ -125,13 +125,13 @@ def _deserialize(cls, mapping: Mapping[XT, YT]):
     def __getitem__(self, __k: XT) -> YT:
         return self.value[__k]
 
-    def __len__(self) -> int:  # pragma no cover
+    def __len__(self) -> int:  # pragma: no cover
         return len(self.value)
 
     def __iter__(self) -> Iterator[XT]:
         return iter(self.value)
 
-    def __repr__(self):  # pragma no cover
+    def __repr__(self):  # pragma: no cover
         return f"<MyMapping: {repr(self.value)}>"
 
 
@@ -148,7 +148,7 @@ def _serialize(self) -> MyMapping[XT, YT]:
     def _deserialize(cls, value: MyMapping[XT, YT]):
         return cls(value)
 
-    def __repr__(self):  # pragma no cover
+    def __repr__(self):  # pragma: no cover
         return f"<MyAnnotatedUserGenericSerializableType: {repr(self.value)}>"