The Ser/Des Protocol🔖
The core of Typical’s protocol resolution logic is the Resolver
. It provides the
central entry-point for our APIs, which allows us to maintain feature symmetry between
the Object API, the
Functional API, and the
The Protocol API. The Resolver
is responsible for the
following work:
- Resolve the type or annotation to an operational runtime description.
- Generate a protocol for deserialization, translation, and validation of incoming data.
- Generate a protocol for the translation and serialization of outgoing data.
The result of this work is a single SerdesProtocol
object which understands how to
interact with inputs and outputs which conform to the type annotation it’s been given.
The Protocol API exposes this object directly, the Object API binds this protocol to the
type definition, and the Functional API uses this protocol internally.
We won’t go over the API of the SerdesProtocol
again, as it has already been described
in detail in Using Typical. Instead, we’re going to focus on how you can
customize the protocol to suite your needs.
Customizing Your Ser/Des Protocol🔖
Typical provides a path for you to customize how your data is transmuted into your custom classes, and how it is dumped back to its primitive form. It all starts with this factory:
typic.flags
🔖
case: Optional[typic.common.Case] = None
Select the case-style for the input/output fields.
exclude: Optional[Iterable[str]] = None
Provide a set of fields which will be excluded from the output.
fields: Union[Tuple[str, ...], Mapping[str, str], None] = None
Ensure a set of fields are included in the output. If given a mapping, provide a mapping to the output field name.
omit: Optional[Tuple[Union[Type, Any], ...]] = None
Provide a tuple of types or values which should be omitted on serialization.
signature_only: bool = False
Restrict the output of serialization to the class signature.
encoder: Callable[..., bytes] = None
Provide a callable which will encode the data to a custom wire format.
decoder: Callable[..., Any] = None
Provide a callable which will decode the data from a custom wire format.
The simplest method for customizing your protocol is via the Protocol API.
Customizing a dataclass Protocol
import dataclasses
import json
import typic
def encode(o):
return json.dumps(o).encode("utf-8-sig")
def decode(o):
return json.loads(o.decode("utf-8-sig"))
@dataclasses.dataclass
class Foo:
bar: str
exclude: str = None
foo = Foo("bar", "exc")
flags = typic.flags(fields={"bar": "Bar"}, exclude=("exclude",), decoder=decode, encoder=encode)
proto = typic.protocol(Foo, flags=flags)
print(proto.primitive(foo))
#> {'Bar': 'bar'}
print(proto.tojson(foo))
#> '{"Bar":"bar"}'
print(proto.encode(foo))
#> b'\xef\xbb\xbf{"Bar": "bar"}'
print(proto.decode(b'\xef\xbb\xbf{"Bar": "bar"}'))
#> Foo(bar='bar', exclude=None)
You can also assign the __serde_flags__
attribute on any class.
Pinned Customization on Classes
class Foo:
__serde_flags__ = typic.flags(fields=("bar", "prop"))
prop: int
bar: str = ""
@property
def prop(self) -> int:
return 0
proto = typic.protocol(Foo)
proto.primitive(Foo())
#> {'prop': 0, 'bar': ''}
Or even pass in pre-defined flags when creating a protocol for an arbitrary annotation.
Pre-defined Flags for Arbitrary Protocols
import typic
from typing import Mapping
flags = typic.flags(case=typic.Case.CAMEL)
mapping_proto = typic.protocol(Mapping, flags=flags)
print(mapping_proto.tojson({"foo_bar": 1}))
#> '{"fooBar":1}'