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): Serializeobjectsin the format.loads(data): Deserializedatato python objects.loads_as(type, input): Deserializedatato python objects as specifiedtype.
Supported types
perde supports the following types.
- Primitive types
intstrfloatboolbytesbytearray
- Generic types
dict/typing.Dictlist/typing.Listset/typing.Setfrozenset/typing.FrozenSettuple/typing.Tupletyping.Optionaltyping.Uniontyping.Any
- Enum types
EnumIntEnumFlagIntFlag
- More built-in types
datetime.datetimedatetime.datedatetime.timedecimal.Decimaluuid.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
intstrfloatboolbytesbytearray
- Generic types
dict/typing.Dictlist/typing.Listset/typing.Setfrozenset/typing.FrozenSettuple/typing.Tupletyping.Optionaltyping.Uniontyping.Any
- Enum types
EnumIntEnumFlagIntFlag
- More built-in types
datetime.datetimedatetime.datedatetime.timedecimal.Decimaluuid.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:
dicttyping.Dicttyping.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:
listtyping.Listtyping.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/frozensettyping.Set/typing.FrozenSettyping.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:
tupletyping.Tupletyping.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:lowercaseUPPERCASEPascalCasecamelCasesnake_caseSCREAMING_SNAKE_CASEkebab-caseSCREAMING-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
dataclassor 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 theperdeattributedefault/perde_defaultset.
perde_skip_serializing: True- Skip serialzing this field.
perde_skip_deserialzing: True- Skip deserializing this field.
- The field must have
default/default_factory, or theperdeattributedefault/perde_defaultset.
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_valueis 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_valueis 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_valueis set.
Benchmark
JSON
YAML
TOML
MessagePack