Attention
This update features:
Support for model-to-model conversion.
Support for attrs and sqlalchemy (integration with many other libraries is coming).
Fully redesigned API helping to follow DRY.
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
- convertsdatetime
to unixtime and vice versaisotime_schema
- convertsdatetime
to string containing ISO 8601. Supported only on Python 3.7+uuid_schema
- convertsUUID
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