Attention

The new major version is out!
The library was renamed to adaptix due to extending of the working scope.

This update features:

  1. Support for model-to-model conversion.

  2. Support for attrs and sqlalchemy (integration with many other libraries is coming).

  3. Fully redesigned API helping to follow DRY.

  4. Performance improvements of up to two times.

Specific types behavior

Structures

Out of the box dataclass factory supports three types of structures:

  • dataclasses

  • TypedDict (total is also checked)

  • other classes with annotated __init__

Feature

dataclass

TypedDict

Class with __init__

Parsing

x

x

x

Name conversion

x

x

x

Omit default

x

Skip internal

x

x

Serializing

x

x

x

Custom parsers and serializers

Not all types are supported out of the box. For example, it is unclear how to parse datetime: it can be represented as unixtime or iso format or something else. If you meet unsupported type you can provide your own parser and serializer:

from datetime import datetime, timezone

from dataclasses import dataclass

from dataclass_factory import Schema, Factory


@dataclass
class Author:
    name: str
    born_at: datetime


def parse_timestamp(data):
    print("parsing timestamp")
    return datetime.fromtimestamp(data, tz=timezone.utc)


unixtime_schema = Schema(
    parser=parse_timestamp,
    serializer=datetime.timestamp
)

factory = Factory(
    schemas={
        datetime: unixtime_schema,
    }
)
expected_author = Author("Petr", datetime(1970, 1, 2, 3, 4, 56, tzinfo=timezone.utc))
data = {'born_at': 97496, 'name': 'Petr'}
author = factory.load(data, Author)
print(author, expected_author)
assert author == expected_author

serialized = factory.dump(author)
assert data == serialized

Common schemas

We have subpackage called dataclass_factory.schema_helpers with some common schemas:

  • unixtime_schema - converts datetime to unixtime and vice versa

  • isotime_schema - converts datetime to string containing ISO 8601. Supported only on Python 3.7+

  • uuid_schema - converts UUID objects to string

Self referenced types

Just place correct annotations and use factory.

from typing import Any, Optional

from dataclasses import dataclass

import dataclass_factory


@dataclass
class LinkedItem:
    value: Any
    next: Optional["LinkedItem"] = None


data = {
    "value": 1,
    "next": {
        "value": 2
    }
}

factory = dataclass_factory.Factory()
items = factory.load(data, LinkedItem)
# items = LinkedItem(1, LinkedItem(2))

Generic classes

Generic classes supported out of the box. The difference is that if no schema found for concrete class, it will be taken for generic.

from dataclasses import dataclass
from typing import TypeVar, Generic

from dataclass_factory import Factory, Schema

T = TypeVar("T")


@dataclass
class FakeFoo(Generic[T]):
    value: T


factory = Factory(schemas={
    FakeFoo[str]: Schema(name_mapping={"value": "s"}),
    FakeFoo: Schema(name_mapping={"value": "i"}),
})
data = {"i": 42, "s": "Hello"}
assert factory.load(data, FakeFoo[str]) == FakeFoo("Hello")  # found schema for concrete type
assert factory.load(data, FakeFoo[int]) == FakeFoo(42)  # schema taken from generic version
assert factory.dump(FakeFoo("hello"), FakeFoo[str]) == {"s": "hello"}  # concrete type is set explicitly
assert factory.dump(FakeFoo("hello")) == {"i": "hello"}  # generic type is detected automatically

Note

Always pass concrete type as as second argument of dump method. Otherwise it will be treated as generic due to type erasure.

Polymorphic parsing

Very common case is to select class based on information in data.

If required fields differ between classes, no configuration required. But sometimes you want to make a selection more explicitly. For example, if data field “type” equals to “item” data should be parsed as Item, if it is “group” then Group class should be used.

For such case you can use type_checker from schema_helpers module. It creates a function, which should be used on pre_parse step. By default it checks type field of data, but you can change it

from typing import Union

from dataclasses import dataclass

from dataclass_factory import Factory, Schema
from dataclass_factory.schema_helpers import type_checker


@dataclass
class Item:
    name: str
    type: str = "item"


@dataclass
class Group:
    name: str
    type: str = "group"


Something = Union[Item, Group]  # Available types

factory = Factory(schemas={
    Item: Schema(pre_parse=type_checker("item", field="type")),
    Group: Schema(pre_parse=type_checker("group")),  # `type` is default name for checked field
})

assert factory.load({"name": "some name", "type": "group"}, Something) == Group("some name")

If you need you own pre_parse function, you can set it as parameter for type_checker factory.

For more complex cases you can write your own function. Just raise ValueError if you detected that current class is not acceptable for provided data, and parser will go to the next one in Union