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)
: Serializeobjects
in the format.loads(data)
: Deserializedata
to python objects.loads_as(type, input)
: Deserializedata
to python objects as specifiedtype
.
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 theperde
attributedefault
/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 theperde
attributedefault
/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