Usage

Install and import perde.

pip install perde
>>> import perde

Assume you have a dataclass,

>>> @dataclass
... class A:
...     a: int
...     b: str

To serialize class A to JSON,

>>> perde.json.dumps(A(a=10, b='x'))
'{"a":10,"b":"x"}'

To deserialize JSON to class A,

>>> perde.json.loads_as(A, '{"a":10,"b":"x"}')
A(a=10, b='x')

To deserialize JSON to a dictionary,

>>> perde.json.loads('{"a":10,"b":"x"}')
{'a': 10, 'b': 'x'}

More formats are supported.

>>> perde.yaml.dumps(A(10, "x"))
'---\na: 10\nb: x'
>>> perde.yaml.loads_as(A, '---\na: 10\nb: x')
A(a=10, b='x')

>>> perde.msgpack.dumps(A(10, "x"))
b'\x82\xa1a\n\xa1b\xa1x'
>>> perde.msgpack.loads_as(A, b'\x82\xa1a\n\xa1b\xa1x')
A(a=10, b='x')

Supported formats

  • JSON (perde.json)
  • YAML (perde.yaml)
  • MessagePack (perde.msgpack)
  • TOML (perde.toml)
  • CBOR
  • Pickle
  • RON
  • BSON
  • Avro
  • JSON5
  • Postcard
  • URL
  • Environment variables
  • AWS Parameter Store
  • S-expressions
  • D-Bus
  • FlexBuffer
  • XML

All the formats provide the three methods:

  • dumps(objects): Serialize objects in the format.
  • loads(data): Deserialize data to python objects.
  • loads_as(type, input): Deserialize data to python objects as specified type.

Supported types

perde supports the following types.

  • Primitive types
    • int
    • str
    • float
    • bool
    • bytes
    • bytearray
  • Generic types
    • dict / typing.Dict
    • list / typing.List
    • set / typing.Set
    • frozenset / typing.FrozenSet
    • tuple / typing.Tuple
    • typing.Optional
    • typing.Union
    • typing.Any
  • Enum types
    • Enum
    • IntEnum
    • Flag
    • IntFlag
  • More built-in types
    • datetime.datetime
    • datetime.date
    • datetime.time
    • decimal.Decimal
    • uuid.UUID
  • dataclass

Deserialization

The supported types can be used to specify as which type the input is parsed. They can be directly set to the first argument of loads_as methods, or can be the member type of dataclass.

Directly set to loads_as

To parse a JSON array as list,

>>> perde.json.loads_as(list, '[97, 98, 99]')
[97, 98, 99]

To parse a JSON array as bytes,

>>> perde.json.loads_as(bytes, '[97, 98, 99]')
b'abc'

To parse a JSON array as a set,

>>> perde.json.loads_as(typing.Set[int], '[97, 98, 99]')
{97, 98, 99}

As a member of dataclass

>>> @dataclass
... class A:
...     a: str
...     b: bytes
...     c: typing.Dict[str, int]

>>> perde.json.loads_as(A, '{"a": "x", "b": [97, 98, 99], "c": {"p": 4, "q": 5}}')
A(a='x', b=b'abc', c={'p': 4, 'q': 5})

Deserializing incompatible types raises an exception from the format module.

>>> @dataclass
... class A:
...     a: int
...     b: str

>>> perde.json.loads_as(A, '{"a": 3, "b": 4}')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
json.JsonError: invalid type: integer `4`, expected a string at line 1 column 15

Serialization

The instances of the supported types can be serialized by dumps methods.

To serialize list to a JSON array,

>>> perde.json.dumps([97, 98, 99])
'[97,98,99]'

To serialize bytes to a JSON array,

>>> perde.json.dumps(b'abc')
'[97,98,99]'

To serialize set to a JSON array,

>> perde.json.dumps({97, 98, 99})
'[97,98,99]'

Supported types

perde supports the following types.

  • Primitive types
    • int
    • str
    • float
    • bool
    • bytes
    • bytearray
  • Generic types
    • dict / typing.Dict
    • list / typing.List
    • set / typing.Set
    • frozenset / typing.FrozenSet
    • tuple / typing.Tuple
    • typing.Optional
    • typing.Union
    • typing.Any
  • Enum types
    • Enum
    • IntEnum
    • Flag
    • IntFlag
  • More built-in types
    • datetime.datetime
    • datetime.date
    • datetime.time
    • decimal.Decimal
    • uuid.UUID
  • dataclass

Deserialization

The supported types can be used to specify as which type the input is parsed. They can be directly set to the first argument of loads_as methods, or can be the member type of dataclass.

Directly set to loads_as

To parse a JSON array as list,

>>> perde.json.loads_as(list, '[97, 98, 99]')
[97, 98, 99]

To parse a JSON array as bytes,

>>> perde.json.loads_as(bytes, '[97, 98, 99]')
b'abc'

To parse a JSON array as a set,

>>> perde.json.loads_as(typing.Set[int], '[97, 98, 99]')
{97, 98, 99}

As a member of dataclass

>>> @dataclass
... class A:
...     a: str
...     b: bytes
...     c: typing.Dict[str, int]

>>> perde.json.loads_as(A, '{"a": "x", "b": [97, 98, 99], "c": {"p": 4, "q": 5}}')
A(a='x', b=b'abc', c={'p': 4, 'q': 5})

Deserializing incompatible types raises an exception from the format module.

>>> @dataclass
... class A:
...     a: int
...     b: str

>>> perde.json.loads_as(A, '{"a": 3, "b": 4}')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
json.JsonError: invalid type: integer `4`, expected a string at line 1 column 15

Serialization

The instances of the supported types can be serialized by dumps methods.

To serialize list to a JSON array,

>>> perde.json.dumps([97, 98, 99])
'[97,98,99]'

To serialize bytes to a JSON array,

>>> perde.json.dumps(b'abc')
'[97,98,99]'

To serialize set to a JSON array,

>> perde.json.dumps({97, 98, 99})
'[97,98,99]'

Dataclass

perde supports serializing/deserializing dataclass. It's serialized to a map in the serialized format (e.g. JSON map).

To serialize the dataclass A to JSON,

>>> @dataclass
... class A:
...     a: str
...     b: int

>>> perde.json.dumps(A("x", 10))
'{"a":"x","b":10}'

To deserialize JSON back to A,

>>> perde.json.loads_as(A, '{"a":"x","b":10}')
A(a='x', b=10)

Nesting is allowed. To serialize the dataclass B which contains A,

>>> @dataclass
... class B:
...     a: float
...     b: A

>>> perde.json.dumps(B(3.33, A("x", 10)))
'{"a":3.33,"b":{"a":"x","b":10}}'

To deserialize B,

>>> perde.json.loads_as(B, '{"a":3.33,"b":{"a":"x","b":10}}')
B(a=3.33, b=A(a='x', b=10))

Generic types

Dictionary

The dictionary types like dict, typing.Dict correspond to a map pattern in serialized format (e.g. JSON map). perde supports the following form of dictionary types:

  • dict
  • typing.Dict
  • typing.Dict[X, Y]
  • dict[X] (since Python 3.9)
  • dict[X, Y] (since Python 3.9)

Using built-in dict,

>>> @dataclass
... class A:
...     a: str
...     b: dict

>>> perde.json.loads_as(A, '{"a": "x", "b": {"x": 3, "y": "hey", "z": true}}')
A(a='x', b={'x': 3, 'y': 'hey', 'z': True})

Using bare typing.Dict,

>>> @dataclass
... class A:
...     a: str
...     b: typing.Dict

>>> perde.json.loads_as(A, '{"a": "x", "b": {"x": 3, "y": "hey", "z": true}}')
A(a='x', b={'x': 3, 'y': 'hey', 'z': True})

Using typing.Dict[X, Y],

>>> @dataclass
... class A:
...     a: str
...     b: typing.Dict[str, float]

>>> perde.json.loads_as(A, '{"a": "x", "b": {"x": 3.0, "y": 1.4, "z": 1.5}}')
A(a='x', b={'x': 3.0, 'y': 1.4, 'z': 1.5})

Using dict[X, Y],

>>> @dataclass
... class A:
...     a: str
...     b: dict[str, float] # doctest: +PY39

>>> perde.json.loads_as(A, '{"a": "x", "b": {"x": 3.0, "y": 1.4, "z": 1.5}}') # doctest: +PY39
A(a='x', b={'x': 3.0, 'y': 1.4, 'z': 1.5})

List

The list types like list, typing.List correspond to a list or array pattern in serialized format (e.g. JSON array). perde supports the following form of list types:

  • list
  • typing.List
  • typing.List[X]
  • list[X] (since Python 3.9)

Using built-in list,

>>> @dataclass
... class A:
...     a: str
...     b: list

>>> perde.json.loads_as(A, '{"a": "x", "b": [1, "a", 3.3]}')
A(a='x', b=[1, 'a', 3.3])

Using bare typing.List,

>>> @dataclass
... class A:
...     a: str
...     b: typing.List

>>> perde.json.loads_as(A, '{"a": "x", "b": [1, 2, 3]}')
A(a='x', b=[1, 2, 3])

Using typing.List[X],

>>> @dataclass
... class A:
...     a: str
...     b: typing.List[int]

>>> perde.json.loads_as(A, '{"a": "x", "b": [1, 2, 3]}')
A(a='x', b=[1, 2, 3])

Using list[X],

>>> @dataclass
... class A:
...     a: str
...     b: list[int] # doctest: +PY39

>>> perde.json.loads_as(A, '{"a": "x", "b": [1, 2, 3]}') # doctest: +PY39
A(a='x', b=[1, 2, 3])

Set

The set types like set, typing.Set correspond to a list or array pattern in serialized format (e.g. JSON array). perde supports the following form of set types:

  • set / frozenset
  • typing.Set / typing.FrozenSet
  • typing.Set[X] / typing.FrozenSet[X]
  • set[X] / frozenset[X] (since Python 3.9)

Using built-in set,

>>> @dataclass
... class A:
...     a: str
...     b: set

>>> perde.json.loads_as(A, '{"a": "x", "b": [true, 2, 3]}')
A(a='x', b={True, 2, 3})

Using bare typing.Set,

>>> @dataclass
... class A:
...     a: str
...     b: typing.Set

>>> perde.json.loads_as(A, '{"a": "x", "b": [true, 2, 3]}')
A(a='x', b={True, 2, 3})

Using typing.Set[X],

>>> @dataclass
... class A:
...     a: str
...     b: typing.Set[int]

>>> perde.json.loads_as(A, '{"a": "x", "b": [1, 2, 3]}')
A(a='x', b={1, 2, 3})

Using set[X],

>>> @dataclass
... class A:
...     a: str
...     b: set[int] # doctest: +PY39

>>> perde.json.loads_as(A, '{"a": "x", "b": [1, 2, 3]}') # doctest: +PY39
A(a='x', b={1, 2, 3})

frozenset and typing.FrozenSet work the same as set and typing.Set.

Tuple

The tuple types like tuple, typing.Tuple correspond to a list or array pattern in serialized format (e.g. JSON array). perde supports the following form of set types:

  • tuple
  • typing.Tuple
  • typing.Tuple[X, Y, ...]
  • tuple[X, Y, ...] (since Python 3.9)

Using built-in tuple,

>>> @dataclass
... class A:
...     a: str
...     b: tuple

>>> perde.json.loads_as(A, '{"a": "x", "b": [1, true, "hello"]}')
A(a='x', b=(1, True, 'hello'))

Using bare typing.Tuple,

>>> @dataclass
... class A:
...     a: str
...     b: typing.Tuple

>>> perde.json.loads_as(A, '{"a": "x", "b": [1, true, "hello"]}')
A(a='x', b=(1, True, 'hello'))

Using typing.Tuple[X, Y, ...],

>>> @dataclass
... class A:
...     a: str
...     b: typing.Tuple[int, bool, str]

>>> perde.json.loads_as(A, '{"a": "x", "b": [1, true, "hello"]}')
A(a='x', b=(1, True, 'hello'))

Using tuple[X, Y, ...],

>>> @dataclass
... class A:
...     a: str
...     b: tuple[int, bool, str] # doctest: +PY39

>>> perde.json.loads_as(A, '{"a": "x", "b": [1, true, "hello"]}') # doctest: +PY39
A(a='x', b=(1, True, 'hello'))

Empty tuple

Use typing.Tuple[()] to explicitly specify the empty tuple.

>>> @dataclass
... class A:
...     a: str
...     b: typing.Tuple[()]

>>> perde.json.loads_as(A, '{"a": "x", "b": []}')
A(a='x', b=())

tuple[()] is also available since Python 3.9.

Optional

typing.Optional allows to parse the field optionally.

>>> @dataclass
... class A:
...     a: str
...     b: typing.Optional[str]

>>> perde.json.loads_as(A, '{"a": "x"}')
A(a='x', b=None)

If the format supports None value (e.g. null in JSON), the None value is accepted.

>>> perde.json.loads_as(A, '{"a": "x", "b": null}')
A(a='x', b=None)

Note that serialization results include null explicitly.

>>> perde.json.dumps(A(a='x', b=None))
'{"a":"x","b":null}'

As the other generic types, typing.Optional without subscription is supported,

>>> @dataclass
... class A:
...     a: str
...     b: typing.Optional

>>> perde.json.loads_as(A, '{"a": "x"}')
A(a='x', b=None)

Union

typing.Union is used when there're multiple possible types for one field.

>>> @dataclass
... class A:
...     a: str
...     b: typing.Union[str, int]

>>> perde.json.loads_as(A, '{"a": "x", "b": 3}')
A(a='x', b=3)

>>> perde.json.loads_as(A, '{"a": "x", "b": "three"}')
A(a='x', b='three')

Bare typing.Union accepts anything including None value, also making the field optional.

>>> @dataclass
... class A:
...     a: str
...     b: typing.Union

>>> perde.json.loads_as(A, '{"a": "x", "b": "anything"}')
A(a='x', b='anything')

The field is optional.

>>> perde.json.loads_as(A, '{"a": "x"}')
A(a='x', b=None)

The field accepts None value.

>>> perde.json.loads_as(A, '{"a": "x", "b": null}')
A(a='x', b=None)

typing.Union cannot be used in schema-less formats which don't have type information themselves.

Any

typing.Any accepts any types including None, also making the field optional.

>>> @dataclass
... class A:
...     a: str
...     b: typing.Any

>>> perde.json.loads_as(A, '{"a": "x", "b": "anything"}')
A(a='x', b='anything')

The field is optional.

>>> perde.json.loads_as(A, '{"a": "x"}')
A(a='x', b=None)

The field accepts None value.

>>> perde.json.loads_as(A, '{"a": "x", "b": null}')
A(a='x', b=None)

Bare typing.Optional and typing.Any behave exactly same as typing.Any. typing.Any cannot be used in schema-less formats which don't have type information themselves.

Enum

Enum types are serialized as the member names by default.

>>> class E(enum.Enum):
...     X = 10
...     Y = 'a'

>>> perde.json.dumps(E.X)
'"X"'
>>> perde.json.loads_as(E, '"Y"')
<E.Y: 'a'>

By using as_value attribute, they are serialized as the member values.

>>> @perde.attr(as_value=True)
... class F(enum.Enum):
...     X = 10
...     Y = 'a'

>>> perde.json.dumps(F.X)
'10'
>>> perde.json.loads_as(F, '"a"')
<F.Y: 'a'>

Date/Time

All datetime.datetime, datetime.date and datetime.time are serialized as string types formatted in ISO 8601.

To serialize datetime,

>>> perde.json.dumps(datetime.datetime(2020, 10, 31, 10, 30, 40, 1234))
'"2020-10-31T10:30:40.001234"'

To deserialize datetime,

>>> perde.json.loads_as(datetime.datetime, '"2020-10-31T10:30:40.001234"')
datetime.datetime(2020, 10, 31, 10, 30, 40, 1234)

To serialize date,

>>> perde.json.dumps(datetime.date(2020, 10, 31))
'"2020-10-31"'

To deserialize date,

>>> perde.json.loads_as(datetime.date, '"2020-10-31"')
datetime.date(2020, 10, 31)

To serialize time,

>>> perde.json.dumps(datetime.time(10, 30, 40, 1234))
'"10:30:40.001234"'

To deserialize time,

>>> perde.json.loads_as(datetime.time, '"10:30:40.001234"')
datetime.time(10, 30, 40, 1234)

Decimal

decimal.Decimal is serialized as string types.

To serialize Decimal,

>>> perde.json.dumps(decimal.Decimal('3.14159265'))
'"3.14159265"'

To deserialize Decimal,

>>> perde.json.loads_as(decimal.Decimal, '"3.14159265"')
Decimal('3.14159265')

UUID

uuid.UUID is serialized as string of hex digits in standard form.

To serialize UUID,

>>> perde.json.dumps(uuid.UUID('a8098c1a-f86e-11da-bd1a-00112444be1e'))
'"a8098c1a-f86e-11da-bd1a-00112444be1e"'

To deserialize UUID,

>>> perde.json.loads_as(uuid.UUID, '"a8098c1a-f86e-11da-bd1a-00112444be1e"')
UUID('a8098c1a-f86e-11da-bd1a-00112444be1e')

Attributes

Attributes allow to modify the way of serialization/deserialization.

For example, to serialize/deserialize the field names as camelCase,

>>> @perde.attr(rename_all="camelCase")
... @dataclass
... class A:
...     foo_bar: int
...     bar_bar: int

>>> perde.json.dumps(A(foo_bar=1, bar_bar=2))
'{"fooBar":1,"barBar":2}'
>>> perde.json.loads_as(A, '{"fooBar":1,"barBar":2}')
A(foo_bar=1, bar_bar=2)

For another example, to skip serializing the field,

>>> @dataclass
... class A:
...     foo_bar: int
...     bar_bar: int = field(metadata = {"perde_skip": True})

>>> perde.json.dumps(A(foo_bar=1, bar_bar=2))
'{"foo_bar":1}'

Attributes can be used with enum as well.

>>> @perde.attr(rename_all = "snake_case")
... class A(enum.Enum):
...     FooBar = 1
...     BarBar = 2

>>> perde.json.dumps(A.BarBar)
'"bar_bar"'
>>> perde.json.loads_as(A, '"foo_bar"')
<A.FooBar: 1>

To use attributes for enum members, inherit perde.Enum/perde.IntEnum instead of enum.Enum/enum.IntEnum.

>>> class A(perde.Enum):
...     FooBar = 1, {"perde_rename": "BooBoo"}
...     BarBar = 2

>>> perde.json.dumps(A.FooBar)
'"BooBoo"'
>>> perde.json.loads_as(A, '"BooBoo"')
<A.FooBar: 1>

Dataclass attributes

The following attributes can be set with dataclass. For example,

>>> @perde.attr(rename="B")
... @dataclass
... class A:
...     a: int
...     b: str

  • rename = "name"
    • Serialize and deserialize classes with the given name instead of the name in Python.
  • rename_all = "string_case"
    • Convert the case of all the field names in the class.
    • The possible values for "string_case" are:
      • lowercase
      • UPPERCASE
      • PascalCase
      • camelCase
      • snake_case
      • SCREAMING_SNAKE_CASE
      • kebab-case
      • SCREAMING-KEBAB-CASE
  • rename_all_serialize = "string_case"
    • Convert the string case only when serialization.
  • rename_all_deserialize = "string_case"
    • Convert the string case only when deserialization.
  • deny_unknown_fields = True
    • Raises an error on deserialization if the input contains unknown fields.
  • default = True
    • When deserialzing, any missing fields in the class are created by their default constructors.

Dataclass field attributes

The following attributes can be set with fields in dataclass. For example,

>>> @dataclass
... class A:
...     a: int
...     b: str = field(metadata = {"perde_skip": True})

  • perde_rename: "name"
    • Serialize and deserialize the field with the given name instead of the name in Python.
  • perde_default: True
    • When deserialzing, if the field is missing, the field is created by its default constructor.
  • perde_flatten: True
    • Flatten the content of this field.
    • The type of the field can be either dataclass or dictionary.
    • If the type is dictionary, all the remaining fields at that point of deserialization are consumed.
  • perde_skip: True
    • Skip serializing or deserializing this field.
    • The field must have default/default_factory, or the perde attribute default/perde_default set.
  • perde_skip_serializing: True
    • Skip serialzing this field.
  • perde_skip_deserialzing: True
    • Skip deserializing this field.
    • The field must have default/default_factory, or the perde attribute default/perde_default set.

Enum attributes

The following attributes can be set with enum. For example,

>>> @perde.attr(rename="B")
... class A(enum.Enum):
...     X = 1
...     Y = 2

  • rename = "name"
    • Serialize and deserialize enums with the given name instead of the name in Python.
  • rename_all = "string_case"
    • Convert the case of all the members in the enum.
    • The possible values are the same as ones for class.
    • This option is ignored when as_value is set.
  • rename_all_serialize = "string_case"
    • Convert the string case only when serialization.
  • rename_all_deserialize = "string_case"
    • Convert the string case only when deserialization.
  • as_value = True
    • Serialize and deserialize enum using the enum value instead of the name.

Enum member attributes

The following attributes can be set with enum members. For example,

>>> class A(perde.Enum):
...     X = 1, {"rename": "Z"}
...     Y = 2

Note that perde.Enum/perde.IntEnum needs to be used instead of enum.Enum/enum.IntEnum.

  • perde_rename: "name"
    • Serialize and deserialize the member with the given name instead of the name in Python.
    • This option is ignored when as_value is set.
  • perde_skip: True
    • Never serialize or deserialize this member.
  • perde_skip_serializing: True
    • Never serialize this member. Serializing this member raises an error.
  • perde_skip_deserialzing: True
    • Never deserialize this member.
  • perde_other: True
    • When deserializing, any unknown members result in this member.
    • This option is ignored when as_value is set.

Benchmark

JSON

YAML

TOML

MessagePack