diff options
author | CoprDistGit <infra@openeuler.org> | 2023-04-10 09:12:17 +0000 |
---|---|---|
committer | CoprDistGit <infra@openeuler.org> | 2023-04-10 09:12:17 +0000 |
commit | 920313f1dde44feeec568440b177665a00f9af60 (patch) | |
tree | 0aa4c39068943376902b8a8c22334ed0dcd515ed | |
parent | a3cd83267a42a3cb73f1e6db37411400e31996d4 (diff) |
automatic import of python-dataclasses-json
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | python-dataclasses-json.spec | 2058 | ||||
-rw-r--r-- | sources | 1 |
3 files changed, 2060 insertions, 0 deletions
@@ -0,0 +1 @@ +/dataclasses-json-0.5.7.tar.gz diff --git a/python-dataclasses-json.spec b/python-dataclasses-json.spec new file mode 100644 index 0000000..d26ddb4 --- /dev/null +++ b/python-dataclasses-json.spec @@ -0,0 +1,2058 @@ +%global _empty_manifest_terminate_build 0 +Name: python-dataclasses-json +Version: 0.5.7 +Release: 1 +Summary: Easily serialize dataclasses to and from JSON +License: MIT +URL: https://github.com/lidatong/dataclasses-json +Source0: https://mirrors.nju.edu.cn/pypi/web/packages/85/94/1b30216f84c48b9e0646833f6f2dd75f1169cc04dc45c48fe39e644c89d5/dataclasses-json-0.5.7.tar.gz +BuildArch: noarch + +Requires: python3-marshmallow +Requires: python3-marshmallow-enum +Requires: python3-typing-inspect +Requires: python3-dataclasses +Requires: python3-pytest +Requires: python3-ipython +Requires: python3-mypy +Requires: python3-hypothesis +Requires: python3-portray +Requires: python3-flake8 +Requires: python3-simplejson +Requires: python3-types-dataclasses + +%description +# Dataclasses JSON + + + +This library provides a simple API for encoding and decoding [dataclasses](https://docs.python.org/3/library/dataclasses.html) to and from JSON. + +It's very easy to get started. + +[README / Documentation website](https://lidatong.github.io/dataclasses-json). Features a navigation bar and search functionality, and should mirror this README exactly -- take a look! + +## Quickstart + +`pip install dataclasses-json` + +```python +from dataclasses import dataclass +from dataclasses_json import dataclass_json + + +@dataclass_json +@dataclass +class Person: + name: str + + +person = Person(name='lidatong') +person.to_json() # '{"name": "lidatong"}' <- this is a string +person.to_dict() # {'name': 'lidatong'} <- this is a dict +Person.from_json('{"name": "lidatong"}') # Person(1) +Person.from_dict({'name': 'lidatong'}) # Person(1) + +# You can also apply _schema validation_ using an alternative API +# This can be useful for "typed" Python code + +Person.from_json('{"name": 42}') # This is ok. 42 is not a `str`, but + # dataclass creation does not validate types +Person.schema().loads('{"name": 42}') # Error! Raises `ValidationError` +``` + +**What if you want to work with camelCase JSON?** + +```python +# same imports as above, with the additional `LetterCase` import +from dataclasses import dataclass +from dataclasses_json import dataclass_json, LetterCase + +@dataclass_json(letter_case=LetterCase.CAMEL) # now all fields are encoded/decoded from camelCase +@dataclass +class ConfiguredSimpleExample: + int_field: int + +ConfiguredSimpleExample(1).to_json() # {"intField": 1} +ConfiguredSimpleExample.from_json('{"intField": 1}') # ConfiguredSimpleExample(1) +``` + +## Supported types + +It's recursive (see caveats below), so you can easily work with nested dataclasses. +In addition to the supported types in the +[py to JSON table](https://docs.python.org/3/library/json.html#py-to-json-table), this library supports the following: + +- any arbitrary [Collection](https://docs.python.org/3/library/collections.abc.html#collections.abc.Collection) type is supported. +[Mapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping) types are encoded as JSON objects and `str` types as JSON strings. +Any other Collection types are encoded into JSON arrays, but decoded into the original collection types. + +- [datetime](https://docs.python.org/3/library/datetime.html#available-types) +objects. `datetime` objects are encoded to `float` (JSON number) using +[timestamp](https://docs.python.org/3/library/datetime.html#datetime.datetime.timestamp). +As specified in the `datetime` docs, if your `datetime` object is naive, it will +assume your system local timezone when calling `.timestamp()`. JSON numbers +corresponding to a `datetime` field in your dataclass are decoded +into a datetime-aware object, with `tzinfo` set to your system local timezone. +Thus, if you encode a datetime-naive object, you will decode into a +datetime-aware object. This is important, because encoding and decoding won't +strictly be inverses. See [this section](#Overriding) if you want to override this default +behavior (for example, if you want to use ISO). + +- [UUID](https://docs.python.org/3/library/uuid.html#uuid.UUID) objects. They +are encoded as `str` (JSON string). + +- [Decimal](https://docs.python.org/3/library/decimal.html) objects. They are +also encoded as `str`. + +**The [latest release](https://github.com/lidatong/dataclasses-json/releases/latest) is compatible with both Python 3.7 and Python 3.6 (with the dataclasses backport).** + +## Usage + +#### Approach 1: Class decorator + +```python +from dataclasses import dataclass +from dataclasses_json import dataclass_json + +@dataclass_json +@dataclass +class Person: + name: str + +lidatong = Person('lidatong') + +# Encoding to JSON +lidatong.to_json() # '{"name": "lidatong"}' + +# Decoding from JSON +Person.from_json('{"name": "lidatong"}') # Person(name='lidatong') +``` + +Note that the `@dataclass_json` decorator must be stacked above the `@dataclass` +decorator (order matters!) + +#### Approach 2: Inherit from a mixin + +```python +from dataclasses import dataclass +from dataclasses_json import DataClassJsonMixin + +@dataclass +class Person(DataClassJsonMixin): + name: str + +lidatong = Person('lidatong') + +# A different example from Approach 1 above, but usage is the exact same +assert Person.from_json(lidatong.to_json()) == lidatong +``` + +Pick whichever approach suits your taste. Note that there is better support for + the mixin approach when using _static analysis_ tools (e.g. linting, typing), + but the differences in implementation will be invisible in _runtime_ usage. + +## How do I... + + + +### Use my dataclass with JSON arrays or objects? + +```python +from dataclasses import dataclass +from dataclasses_json import dataclass_json + +@dataclass_json +@dataclass +class Person: + name: str +``` + +**Encode into a JSON array containing instances of my Data Class** + +```python +people_json = [Person('lidatong')] +Person.schema().dumps(people_json, many=True) # '[{"name": "lidatong"}]' +``` + +**Decode a JSON array containing instances of my Data Class** + +```python +people_json = '[{"name": "lidatong"}]' +Person.schema().loads(people_json, many=True) # [Person(name='lidatong')] +``` + +**Encode as part of a larger JSON object containing my Data Class (e.g. an HTTP +request/response)** + +```python +import json + +response_dict = { + 'response': { + 'person': Person('lidatong').to_dict() + } +} + +response_json = json.dumps(response_dict) +``` + +In this case, we do two steps. First, we encode the dataclass into a +**python dictionary** rather than a JSON string, using `.to_dict`. + +Second, we leverage the built-in `json.dumps` to serialize our `dataclass` into +a JSON string. + +**Decode as part of a larger JSON object containing my Data Class (e.g. an HTTP +response)** + +```python +import json + +response_dict = json.loads('{"response": {"person": {"name": "lidatong"}}}') + +person_dict = response_dict['response'] + +person = Person.from_dict(person_dict) +``` + +In a similar vein to encoding above, we leverage the built-in `json` module. + +First, call `json.loads` to read the entire JSON object into a +dictionary. We then access the key of the value containing the encoded dict of +our `Person` that we want to decode (`response_dict['response']`). + +Second, we load in the dictionary using `Person.from_dict`. + + +### Encode or decode into Python lists/dictionaries rather than JSON? + +This can be by calling `.schema()` and then using the corresponding +encoder/decoder methods, ie. `.load(...)`/`.dump(...)`. + +**Encode into a single Python dictionary** + +```python +person = Person('lidatong') +person.to_dict() # {'name': 'lidatong'} +``` + +**Encode into a list of Python dictionaries** + +```python +people = [Person('lidatong')] +Person.schema().dump(people, many=True) # [{'name': 'lidatong'}] +``` + +**Decode a dictionary into a single dataclass instance** + +```python +person_dict = {'name': 'lidatong'} +Person.from_dict(person_dict) # Person(name='lidatong') +``` + +**Decode a list of dictionaries into a list of dataclass instances** + +```python +people_dicts = [{"name": "lidatong"}] +Person.schema().load(people_dicts, many=True) # [Person(name='lidatong')] +``` + +### Encode or decode from camelCase (or kebab-case)? + +JSON letter case by convention is camelCase, in Python members are by convention snake_case. + +You can configure it to encode/decode from other casing schemes at both the class level and the field level. + +```python +from dataclasses import dataclass, field + +from dataclasses_json import LetterCase, config, dataclass_json + + +# changing casing at the class level +@dataclass_json(letter_case=LetterCase.CAMEL) +@dataclass +class Person: + given_name: str + family_name: str + +Person('Alice', 'Liddell').to_json() # '{"givenName": "Alice"}' +Person.from_json('{"givenName": "Alice", "familyName": "Liddell"}') # Person('Alice', 'Liddell') + +# at the field level +@dataclass_json +@dataclass +class Person: + given_name: str = field(metadata=config(letter_case=LetterCase.CAMEL)) + family_name: str + +Person('Alice', 'Liddell').to_json() # '{"givenName": "Alice"}' +# notice how the `family_name` field is still snake_case, because it wasn't configured above +Person.from_json('{"givenName": "Alice", "family_name": "Liddell"}') # Person('Alice', 'Liddell') +``` + +**This library assumes your field follows the Python convention of snake_case naming.** +If your field is not `snake_case` to begin with and you attempt to parameterize `LetterCase`, +the behavior of encoding/decoding is undefined (most likely it will result in subtle bugs). + +### Encode or decode using a different name + +```python +from dataclasses import dataclass, field + +from dataclasses_json import config, dataclass_json + +@dataclass_json +@dataclass +class Person: + given_name: str = field(metadata=config(field_name="overriddenGivenName")) + +Person(given_name="Alice") # Person('Alice') +Person.from_json('{"overriddenGivenName": "Alice"}') # Person('Alice') +Person('Alice').to_json() # {"overriddenGivenName": "Alice"} +``` + +### Handle missing or optional field values when decoding? + +By default, any fields in your dataclass that use `default` or +`default_factory` will have the values filled with the provided default, if the +corresponding field is missing from the JSON you're decoding. + +**Decode JSON with missing field** + +```python +@dataclass_json +@dataclass +class Student: + id: int + name: str = 'student' + +Student.from_json('{"id": 1}') # Student(id=1, name='student') +``` + +Notice `from_json` filled the field `name` with the specified default 'student' +when it was missing from the JSON. + +Sometimes you have fields that are typed as `Optional`, but you don't +necessarily want to assign a default. In that case, you can use the +`infer_missing` kwarg to make `from_json` infer the missing field value as `None`. + +**Decode optional field without default** + +```python +@dataclass_json +@dataclass +class Tutor: + id: int + student: Optional[Student] = None + +Tutor.from_json('{"id": 1}') # Tutor(id=1, student=None) +``` + +Personally I recommend you leverage dataclass defaults rather than using +`infer_missing`, but if for some reason you need to decouple the behavior of +JSON decoding from the field's default value, this will allow you to do so. + + +### Handle unknown / extraneous fields in JSON? + +By default, it is up to the implementation what happens when a `json_dataclass` receives input parameters that are not defined. +(the `from_dict` method ignores them, when loading using `schema()` a ValidationError is raised.) +There are three ways to customize this behavior. + +Assume you want to instantiate a dataclass with the following dictionary: +```python +dump_dict = {"endpoint": "some_api_endpoint", "data": {"foo": 1, "bar": "2"}, "undefined_field_name": [1, 2, 3]} +``` + +1. You can enforce to always raise an error by setting the `undefined` keyword to `Undefined.RAISE` + (`'RAISE'` as a case-insensitive string works as well). Of course it works normally if you don't pass any undefined parameters. + +```python +from dataclasses_json import Undefined + +@dataclass_json(undefined=Undefined.RAISE) +@dataclass() +class ExactAPIDump: + endpoint: str + data: Dict[str, Any] + +dump = ExactAPIDump.from_dict(dump_dict) # raises UndefinedParameterError +``` + +2. You can simply ignore any undefined parameters by setting the `undefined` keyword to `Undefined.EXCLUDE` + (`'EXCLUDE'` as a case-insensitive string works as well). Note that you will not be able to retrieve them using `to_dict`: + +```python +from dataclasses_json import Undefined + +@dataclass_json(undefined=Undefined.EXCLUDE) +@dataclass() +class DontCareAPIDump: + endpoint: str + data: Dict[str, Any] + +dump = DontCareAPIDump.from_dict(dump_dict) # DontCareAPIDump(endpoint='some_api_endpoint', data={'foo': 1, 'bar': '2'}) +dump.to_dict() # {"endpoint": "some_api_endpoint", "data": {"foo": 1, "bar": "2"}} +``` + +3. You can save them in a catch-all field and do whatever needs to be done later. Simply set the `undefined` +keyword to `Undefined.INCLUDE` (`'INCLUDE'` as a case-insensitive string works as well) and define a field +of type `CatchAll` where all unknown values will end up. + This simply represents a dictionary that can hold anything. + If there are no undefined parameters, this will be an empty dictionary. + +```python +from dataclasses_json import Undefined, CatchAll + +@dataclass_json(undefined=Undefined.INCLUDE) +@dataclass() +class UnknownAPIDump: + endpoint: str + data: Dict[str, Any] + unknown_things: CatchAll + +dump = UnknownAPIDump.from_dict(dump_dict) # UnknownAPIDump(endpoint='some_api_endpoint', data={'foo': 1, 'bar': '2'}, unknown_things={'undefined_field_name': [1, 2, 3]}) +dump.to_dict() # {'endpoint': 'some_api_endpoint', 'data': {'foo': 1, 'bar': '2'}, 'undefined_field_name': [1, 2, 3]} +``` + +Notes: +- When using `Undefined.INCLUDE`, an `UndefinedParameterError` will be raised if you don't specify +exactly one field of type `CatchAll`. +- Note that `LetterCase` does not affect values written into the `CatchAll` field, they will be as they are given. +- When specifying a default (or a default factory) for the the `CatchAll`-field, e.g. `unknown_things: CatchAll = None`, the default value will be used instead of an empty dict if there are no undefined parameters. +- Calling __init__ with non-keyword arguments resolves the arguments to the defined fields and writes everything else into the catch-all field. + +4. All 3 options work as well using `schema().loads` and `schema().dumps`, as long as you don't overwrite it by specifying `schema(unknown=<a marshmallow value>)`. +marshmallow uses the same 3 keywords ['include', 'exclude', 'raise'](https://marshmallow.readthedocs.io/en/stable/quickstart.html#handling-unknown-fields). + +5. All 3 operations work as well using `__init__`, e.g. `UnknownAPIDump(**dump_dict)` will **not** raise a `TypeError`, but write all unknown values to the field tagged as `CatchAll`. + Classes tagged with `EXCLUDE` will also simply ignore unknown parameters. Note that classes tagged as `RAISE` still raise a `TypeError`, and **not** a `UndefinedParameterError` if supplied with unknown keywords. + + +### Override the default encode / decode / marshmallow field of a specific field? + +See [Overriding](#Overriding) + +### Handle recursive dataclasses? +Object hierarchies where fields are of the type that they are declared within require a small +type hinting trick to declare the forward reference. +```python +from typing import Optional +from dataclasses import dataclass +from dataclasses_json import dataclass_json + +@dataclass_json +@dataclass +class Tree(): + value: str + left: Optional['Tree'] + right: Optional['Tree'] +``` + +Avoid using +```python +from __future__ import annotations +``` +as it will cause problems with the way dataclasses_json accesses the type annotations. + + +## Marshmallow interop + +Using the `dataclass_json` decorator or mixing in `DataClassJsonMixin` will +provide you with an additional method `.schema()`. + +`.schema()` generates a schema exactly equivalent to manually creating a +marshmallow schema for your dataclass. You can reference the [marshmallow API docs](https://marshmallow.readthedocs.io/en/3.0/api_reference.html#schema) +to learn other ways you can use the schema returned by `.schema()`. + +You can pass in the exact same arguments to `.schema()` that you would when +constructing a `PersonSchema` instance, e.g. `.schema(many=True)`, and they will +get passed through to the marshmallow schema. + + +```python +from dataclasses import dataclass +from dataclasses_json import dataclass_json + +@dataclass_json +@dataclass +class Person: + name: str + +# You don't need to do this - it's generated for you by `.schema()`! +from marshmallow import Schema, fields + +class PersonSchema(Schema): + name = fields.Str() +``` + +Briefly, on what's going on under the hood in the above examples: calling +`.schema()` will have this library generate a +[marshmallow schema]('https://marshmallow.readthedocs.io/en/3.0/api_reference.html#schema) +for you. It also fills in the corresponding object hook, so that marshmallow +will create an instance of your Data Class on `load` (e.g. +`Person.schema().load` returns a `Person`) rather than a `dict`, which it does +by default in marshmallow. + +**Performance note** + +`.schema()` is not cached (it generates the schema on every call), so if you +have a nested Data Class you may want to save the result to a variable to +avoid re-generation of the schema on every usage. + +```python +person_schema = Person.schema() +person_schema.dump(people, many=True) + +# later in the code... + +person_schema.dump(person) +``` + +## Overriding / Extending + +#### Overriding + +For example, you might want to encode/decode `datetime` objects using ISO format +rather than the default `timestamp`. + +```python +from dataclasses import dataclass, field +from dataclasses_json import dataclass_json, config +from datetime import datetime +from marshmallow import fields + +@dataclass_json +@dataclass +class DataClassWithIsoDatetime: + created_at: datetime = field( + metadata=config( + encoder=datetime.isoformat, + decoder=datetime.fromisoformat, + mm_field=fields.DateTime(format='iso') + ) + ) +``` + +#### Extending + +Similarly, you might want to extend `dataclasses_json` to encode `date` objects. + +```python +from dataclasses import dataclass, field +from dataclasses_json import dataclass_json, config +from datetime import date +from marshmallow import fields + +@dataclass_json +@dataclass +class DataClassWithIsoDatetime: + created_at: date = field( + metadata=config( + encoder= date.isoformat, + decoder= date.fromisoformat, + mm_field= fields.DateTime(format='iso') + )) +``` + +As you can see, you can **override** or **extend** the default codecs by providing a "hook" via a +callable: +- `encoder`: a callable, which will be invoked to convert the field value when encoding to JSON +- `decoder`: a callable, which will be invoked to convert the JSON value when decoding from JSON +- `mm_field`: a marshmallow field, which will affect the behavior of any operations involving `.schema()` + +Note that these hooks will be invoked regardless if you're using +`.to_json`/`dump`/`dumps` +and `.from_json`/`load`/`loads`. So apply overrides / extensions judiciously, making sure to +carefully consider whether the interaction of the encode/decode/mm_field is consistent with what you expect! + + +#### What if I have other dataclass field extensions that rely on `metadata` + +All the `dataclasses_json.config` does is return a mapping, namespaced under the key `'dataclasses_json'`. + +Say there's another module, `other_dataclass_package` that uses metadata. Here's how you solve your problem: + +```python +metadata = {'other_dataclass_package': 'some metadata...'} # pre-existing metadata for another dataclass package +dataclass_json_config = config( + encoder=datetime.isoformat, + decoder=datetime.fromisoformat, + mm_field=fields.DateTime(format='iso') + ) +metadata.update(dataclass_json_config) + +@dataclass_json +@dataclass +class DataClassWithIsoDatetime: + created_at: datetime = field(metadata=metadata) +``` + +You can also manually specify the dataclass_json configuration mapping. + +```python +@dataclass_json +@dataclass +class DataClassWithIsoDatetime: + created_at: date = field( + metadata={'dataclasses_json': { + 'encoder': date.isoformat, + 'decoder': date.fromisoformat, + 'mm_field': fields.DateTime(format='iso') + }} + ) +``` + +## A larger example + +```python +from dataclasses import dataclass +from dataclasses_json import dataclass_json + +from typing import List + +@dataclass_json +@dataclass(frozen=True) +class Minion: + name: str + + +@dataclass_json +@dataclass(frozen=True) +class Boss: + minions: List[Minion] + +boss = Boss([Minion('evil minion'), Minion('very evil minion')]) +boss_json = """ +{ + "minions": [ + { + "name": "evil minion" + }, + { + "name": "very evil minion" + } + ] +} +""".strip() + +assert boss.to_json(indent=4) == boss_json +assert Boss.from_json(boss_json) == boss +``` + +## Performance + +Take a look at [this issue](https://github.com/lidatong/dataclasses-json/issues/228) + +## Versioning + +Note this library is still pre-1.0.0 (SEMVER). + +The current convention is: +- **PATCH** version upgrades for bug fixes and minor feature additions. +- **MINOR** version upgrades for big API features and breaking changes. + +Once this library is 1.0.0, it will follow standard SEMVER conventions. + + +## Roadmap + +Currently the focus is on investigating and fixing bugs in this library, working +on performance, and finishing [this issue](https://github.com/lidatong/dataclasses-json/issues/31). + +That said, if you think there's a feature missing / something new needed in the +library, please see the contributing section below. + + +## Contributing + +First of all, thank you for being interested in contributing to this library. +I really appreciate you taking the time to work on this project. + +- If you're just interested in getting into the code, a good place to start are +issues tagged as bugs. +- If introducing a new feature, especially one that modifies the public API, +consider submitting an issue for discussion before a PR. Please also take a look +at existing issues / PRs to see what you're proposing has already been covered +before / exists. +- I like to follow the commit conventions documented [here](https://www.conventionalcommits.org/en/v1.0.0/#summary) + + + + +%package -n python3-dataclasses-json +Summary: Easily serialize dataclasses to and from JSON +Provides: python-dataclasses-json +BuildRequires: python3-devel +BuildRequires: python3-setuptools +BuildRequires: python3-pip +%description -n python3-dataclasses-json +# Dataclasses JSON + + + +This library provides a simple API for encoding and decoding [dataclasses](https://docs.python.org/3/library/dataclasses.html) to and from JSON. + +It's very easy to get started. + +[README / Documentation website](https://lidatong.github.io/dataclasses-json). Features a navigation bar and search functionality, and should mirror this README exactly -- take a look! + +## Quickstart + +`pip install dataclasses-json` + +```python +from dataclasses import dataclass +from dataclasses_json import dataclass_json + + +@dataclass_json +@dataclass +class Person: + name: str + + +person = Person(name='lidatong') +person.to_json() # '{"name": "lidatong"}' <- this is a string +person.to_dict() # {'name': 'lidatong'} <- this is a dict +Person.from_json('{"name": "lidatong"}') # Person(1) +Person.from_dict({'name': 'lidatong'}) # Person(1) + +# You can also apply _schema validation_ using an alternative API +# This can be useful for "typed" Python code + +Person.from_json('{"name": 42}') # This is ok. 42 is not a `str`, but + # dataclass creation does not validate types +Person.schema().loads('{"name": 42}') # Error! Raises `ValidationError` +``` + +**What if you want to work with camelCase JSON?** + +```python +# same imports as above, with the additional `LetterCase` import +from dataclasses import dataclass +from dataclasses_json import dataclass_json, LetterCase + +@dataclass_json(letter_case=LetterCase.CAMEL) # now all fields are encoded/decoded from camelCase +@dataclass +class ConfiguredSimpleExample: + int_field: int + +ConfiguredSimpleExample(1).to_json() # {"intField": 1} +ConfiguredSimpleExample.from_json('{"intField": 1}') # ConfiguredSimpleExample(1) +``` + +## Supported types + +It's recursive (see caveats below), so you can easily work with nested dataclasses. +In addition to the supported types in the +[py to JSON table](https://docs.python.org/3/library/json.html#py-to-json-table), this library supports the following: + +- any arbitrary [Collection](https://docs.python.org/3/library/collections.abc.html#collections.abc.Collection) type is supported. +[Mapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping) types are encoded as JSON objects and `str` types as JSON strings. +Any other Collection types are encoded into JSON arrays, but decoded into the original collection types. + +- [datetime](https://docs.python.org/3/library/datetime.html#available-types) +objects. `datetime` objects are encoded to `float` (JSON number) using +[timestamp](https://docs.python.org/3/library/datetime.html#datetime.datetime.timestamp). +As specified in the `datetime` docs, if your `datetime` object is naive, it will +assume your system local timezone when calling `.timestamp()`. JSON numbers +corresponding to a `datetime` field in your dataclass are decoded +into a datetime-aware object, with `tzinfo` set to your system local timezone. +Thus, if you encode a datetime-naive object, you will decode into a +datetime-aware object. This is important, because encoding and decoding won't +strictly be inverses. See [this section](#Overriding) if you want to override this default +behavior (for example, if you want to use ISO). + +- [UUID](https://docs.python.org/3/library/uuid.html#uuid.UUID) objects. They +are encoded as `str` (JSON string). + +- [Decimal](https://docs.python.org/3/library/decimal.html) objects. They are +also encoded as `str`. + +**The [latest release](https://github.com/lidatong/dataclasses-json/releases/latest) is compatible with both Python 3.7 and Python 3.6 (with the dataclasses backport).** + +## Usage + +#### Approach 1: Class decorator + +```python +from dataclasses import dataclass +from dataclasses_json import dataclass_json + +@dataclass_json +@dataclass +class Person: + name: str + +lidatong = Person('lidatong') + +# Encoding to JSON +lidatong.to_json() # '{"name": "lidatong"}' + +# Decoding from JSON +Person.from_json('{"name": "lidatong"}') # Person(name='lidatong') +``` + +Note that the `@dataclass_json` decorator must be stacked above the `@dataclass` +decorator (order matters!) + +#### Approach 2: Inherit from a mixin + +```python +from dataclasses import dataclass +from dataclasses_json import DataClassJsonMixin + +@dataclass +class Person(DataClassJsonMixin): + name: str + +lidatong = Person('lidatong') + +# A different example from Approach 1 above, but usage is the exact same +assert Person.from_json(lidatong.to_json()) == lidatong +``` + +Pick whichever approach suits your taste. Note that there is better support for + the mixin approach when using _static analysis_ tools (e.g. linting, typing), + but the differences in implementation will be invisible in _runtime_ usage. + +## How do I... + + + +### Use my dataclass with JSON arrays or objects? + +```python +from dataclasses import dataclass +from dataclasses_json import dataclass_json + +@dataclass_json +@dataclass +class Person: + name: str +``` + +**Encode into a JSON array containing instances of my Data Class** + +```python +people_json = [Person('lidatong')] +Person.schema().dumps(people_json, many=True) # '[{"name": "lidatong"}]' +``` + +**Decode a JSON array containing instances of my Data Class** + +```python +people_json = '[{"name": "lidatong"}]' +Person.schema().loads(people_json, many=True) # [Person(name='lidatong')] +``` + +**Encode as part of a larger JSON object containing my Data Class (e.g. an HTTP +request/response)** + +```python +import json + +response_dict = { + 'response': { + 'person': Person('lidatong').to_dict() + } +} + +response_json = json.dumps(response_dict) +``` + +In this case, we do two steps. First, we encode the dataclass into a +**python dictionary** rather than a JSON string, using `.to_dict`. + +Second, we leverage the built-in `json.dumps` to serialize our `dataclass` into +a JSON string. + +**Decode as part of a larger JSON object containing my Data Class (e.g. an HTTP +response)** + +```python +import json + +response_dict = json.loads('{"response": {"person": {"name": "lidatong"}}}') + +person_dict = response_dict['response'] + +person = Person.from_dict(person_dict) +``` + +In a similar vein to encoding above, we leverage the built-in `json` module. + +First, call `json.loads` to read the entire JSON object into a +dictionary. We then access the key of the value containing the encoded dict of +our `Person` that we want to decode (`response_dict['response']`). + +Second, we load in the dictionary using `Person.from_dict`. + + +### Encode or decode into Python lists/dictionaries rather than JSON? + +This can be by calling `.schema()` and then using the corresponding +encoder/decoder methods, ie. `.load(...)`/`.dump(...)`. + +**Encode into a single Python dictionary** + +```python +person = Person('lidatong') +person.to_dict() # {'name': 'lidatong'} +``` + +**Encode into a list of Python dictionaries** + +```python +people = [Person('lidatong')] +Person.schema().dump(people, many=True) # [{'name': 'lidatong'}] +``` + +**Decode a dictionary into a single dataclass instance** + +```python +person_dict = {'name': 'lidatong'} +Person.from_dict(person_dict) # Person(name='lidatong') +``` + +**Decode a list of dictionaries into a list of dataclass instances** + +```python +people_dicts = [{"name": "lidatong"}] +Person.schema().load(people_dicts, many=True) # [Person(name='lidatong')] +``` + +### Encode or decode from camelCase (or kebab-case)? + +JSON letter case by convention is camelCase, in Python members are by convention snake_case. + +You can configure it to encode/decode from other casing schemes at both the class level and the field level. + +```python +from dataclasses import dataclass, field + +from dataclasses_json import LetterCase, config, dataclass_json + + +# changing casing at the class level +@dataclass_json(letter_case=LetterCase.CAMEL) +@dataclass +class Person: + given_name: str + family_name: str + +Person('Alice', 'Liddell').to_json() # '{"givenName": "Alice"}' +Person.from_json('{"givenName": "Alice", "familyName": "Liddell"}') # Person('Alice', 'Liddell') + +# at the field level +@dataclass_json +@dataclass +class Person: + given_name: str = field(metadata=config(letter_case=LetterCase.CAMEL)) + family_name: str + +Person('Alice', 'Liddell').to_json() # '{"givenName": "Alice"}' +# notice how the `family_name` field is still snake_case, because it wasn't configured above +Person.from_json('{"givenName": "Alice", "family_name": "Liddell"}') # Person('Alice', 'Liddell') +``` + +**This library assumes your field follows the Python convention of snake_case naming.** +If your field is not `snake_case` to begin with and you attempt to parameterize `LetterCase`, +the behavior of encoding/decoding is undefined (most likely it will result in subtle bugs). + +### Encode or decode using a different name + +```python +from dataclasses import dataclass, field + +from dataclasses_json import config, dataclass_json + +@dataclass_json +@dataclass +class Person: + given_name: str = field(metadata=config(field_name="overriddenGivenName")) + +Person(given_name="Alice") # Person('Alice') +Person.from_json('{"overriddenGivenName": "Alice"}') # Person('Alice') +Person('Alice').to_json() # {"overriddenGivenName": "Alice"} +``` + +### Handle missing or optional field values when decoding? + +By default, any fields in your dataclass that use `default` or +`default_factory` will have the values filled with the provided default, if the +corresponding field is missing from the JSON you're decoding. + +**Decode JSON with missing field** + +```python +@dataclass_json +@dataclass +class Student: + id: int + name: str = 'student' + +Student.from_json('{"id": 1}') # Student(id=1, name='student') +``` + +Notice `from_json` filled the field `name` with the specified default 'student' +when it was missing from the JSON. + +Sometimes you have fields that are typed as `Optional`, but you don't +necessarily want to assign a default. In that case, you can use the +`infer_missing` kwarg to make `from_json` infer the missing field value as `None`. + +**Decode optional field without default** + +```python +@dataclass_json +@dataclass +class Tutor: + id: int + student: Optional[Student] = None + +Tutor.from_json('{"id": 1}') # Tutor(id=1, student=None) +``` + +Personally I recommend you leverage dataclass defaults rather than using +`infer_missing`, but if for some reason you need to decouple the behavior of +JSON decoding from the field's default value, this will allow you to do so. + + +### Handle unknown / extraneous fields in JSON? + +By default, it is up to the implementation what happens when a `json_dataclass` receives input parameters that are not defined. +(the `from_dict` method ignores them, when loading using `schema()` a ValidationError is raised.) +There are three ways to customize this behavior. + +Assume you want to instantiate a dataclass with the following dictionary: +```python +dump_dict = {"endpoint": "some_api_endpoint", "data": {"foo": 1, "bar": "2"}, "undefined_field_name": [1, 2, 3]} +``` + +1. You can enforce to always raise an error by setting the `undefined` keyword to `Undefined.RAISE` + (`'RAISE'` as a case-insensitive string works as well). Of course it works normally if you don't pass any undefined parameters. + +```python +from dataclasses_json import Undefined + +@dataclass_json(undefined=Undefined.RAISE) +@dataclass() +class ExactAPIDump: + endpoint: str + data: Dict[str, Any] + +dump = ExactAPIDump.from_dict(dump_dict) # raises UndefinedParameterError +``` + +2. You can simply ignore any undefined parameters by setting the `undefined` keyword to `Undefined.EXCLUDE` + (`'EXCLUDE'` as a case-insensitive string works as well). Note that you will not be able to retrieve them using `to_dict`: + +```python +from dataclasses_json import Undefined + +@dataclass_json(undefined=Undefined.EXCLUDE) +@dataclass() +class DontCareAPIDump: + endpoint: str + data: Dict[str, Any] + +dump = DontCareAPIDump.from_dict(dump_dict) # DontCareAPIDump(endpoint='some_api_endpoint', data={'foo': 1, 'bar': '2'}) +dump.to_dict() # {"endpoint": "some_api_endpoint", "data": {"foo": 1, "bar": "2"}} +``` + +3. You can save them in a catch-all field and do whatever needs to be done later. Simply set the `undefined` +keyword to `Undefined.INCLUDE` (`'INCLUDE'` as a case-insensitive string works as well) and define a field +of type `CatchAll` where all unknown values will end up. + This simply represents a dictionary that can hold anything. + If there are no undefined parameters, this will be an empty dictionary. + +```python +from dataclasses_json import Undefined, CatchAll + +@dataclass_json(undefined=Undefined.INCLUDE) +@dataclass() +class UnknownAPIDump: + endpoint: str + data: Dict[str, Any] + unknown_things: CatchAll + +dump = UnknownAPIDump.from_dict(dump_dict) # UnknownAPIDump(endpoint='some_api_endpoint', data={'foo': 1, 'bar': '2'}, unknown_things={'undefined_field_name': [1, 2, 3]}) +dump.to_dict() # {'endpoint': 'some_api_endpoint', 'data': {'foo': 1, 'bar': '2'}, 'undefined_field_name': [1, 2, 3]} +``` + +Notes: +- When using `Undefined.INCLUDE`, an `UndefinedParameterError` will be raised if you don't specify +exactly one field of type `CatchAll`. +- Note that `LetterCase` does not affect values written into the `CatchAll` field, they will be as they are given. +- When specifying a default (or a default factory) for the the `CatchAll`-field, e.g. `unknown_things: CatchAll = None`, the default value will be used instead of an empty dict if there are no undefined parameters. +- Calling __init__ with non-keyword arguments resolves the arguments to the defined fields and writes everything else into the catch-all field. + +4. All 3 options work as well using `schema().loads` and `schema().dumps`, as long as you don't overwrite it by specifying `schema(unknown=<a marshmallow value>)`. +marshmallow uses the same 3 keywords ['include', 'exclude', 'raise'](https://marshmallow.readthedocs.io/en/stable/quickstart.html#handling-unknown-fields). + +5. All 3 operations work as well using `__init__`, e.g. `UnknownAPIDump(**dump_dict)` will **not** raise a `TypeError`, but write all unknown values to the field tagged as `CatchAll`. + Classes tagged with `EXCLUDE` will also simply ignore unknown parameters. Note that classes tagged as `RAISE` still raise a `TypeError`, and **not** a `UndefinedParameterError` if supplied with unknown keywords. + + +### Override the default encode / decode / marshmallow field of a specific field? + +See [Overriding](#Overriding) + +### Handle recursive dataclasses? +Object hierarchies where fields are of the type that they are declared within require a small +type hinting trick to declare the forward reference. +```python +from typing import Optional +from dataclasses import dataclass +from dataclasses_json import dataclass_json + +@dataclass_json +@dataclass +class Tree(): + value: str + left: Optional['Tree'] + right: Optional['Tree'] +``` + +Avoid using +```python +from __future__ import annotations +``` +as it will cause problems with the way dataclasses_json accesses the type annotations. + + +## Marshmallow interop + +Using the `dataclass_json` decorator or mixing in `DataClassJsonMixin` will +provide you with an additional method `.schema()`. + +`.schema()` generates a schema exactly equivalent to manually creating a +marshmallow schema for your dataclass. You can reference the [marshmallow API docs](https://marshmallow.readthedocs.io/en/3.0/api_reference.html#schema) +to learn other ways you can use the schema returned by `.schema()`. + +You can pass in the exact same arguments to `.schema()` that you would when +constructing a `PersonSchema` instance, e.g. `.schema(many=True)`, and they will +get passed through to the marshmallow schema. + + +```python +from dataclasses import dataclass +from dataclasses_json import dataclass_json + +@dataclass_json +@dataclass +class Person: + name: str + +# You don't need to do this - it's generated for you by `.schema()`! +from marshmallow import Schema, fields + +class PersonSchema(Schema): + name = fields.Str() +``` + +Briefly, on what's going on under the hood in the above examples: calling +`.schema()` will have this library generate a +[marshmallow schema]('https://marshmallow.readthedocs.io/en/3.0/api_reference.html#schema) +for you. It also fills in the corresponding object hook, so that marshmallow +will create an instance of your Data Class on `load` (e.g. +`Person.schema().load` returns a `Person`) rather than a `dict`, which it does +by default in marshmallow. + +**Performance note** + +`.schema()` is not cached (it generates the schema on every call), so if you +have a nested Data Class you may want to save the result to a variable to +avoid re-generation of the schema on every usage. + +```python +person_schema = Person.schema() +person_schema.dump(people, many=True) + +# later in the code... + +person_schema.dump(person) +``` + +## Overriding / Extending + +#### Overriding + +For example, you might want to encode/decode `datetime` objects using ISO format +rather than the default `timestamp`. + +```python +from dataclasses import dataclass, field +from dataclasses_json import dataclass_json, config +from datetime import datetime +from marshmallow import fields + +@dataclass_json +@dataclass +class DataClassWithIsoDatetime: + created_at: datetime = field( + metadata=config( + encoder=datetime.isoformat, + decoder=datetime.fromisoformat, + mm_field=fields.DateTime(format='iso') + ) + ) +``` + +#### Extending + +Similarly, you might want to extend `dataclasses_json` to encode `date` objects. + +```python +from dataclasses import dataclass, field +from dataclasses_json import dataclass_json, config +from datetime import date +from marshmallow import fields + +@dataclass_json +@dataclass +class DataClassWithIsoDatetime: + created_at: date = field( + metadata=config( + encoder= date.isoformat, + decoder= date.fromisoformat, + mm_field= fields.DateTime(format='iso') + )) +``` + +As you can see, you can **override** or **extend** the default codecs by providing a "hook" via a +callable: +- `encoder`: a callable, which will be invoked to convert the field value when encoding to JSON +- `decoder`: a callable, which will be invoked to convert the JSON value when decoding from JSON +- `mm_field`: a marshmallow field, which will affect the behavior of any operations involving `.schema()` + +Note that these hooks will be invoked regardless if you're using +`.to_json`/`dump`/`dumps` +and `.from_json`/`load`/`loads`. So apply overrides / extensions judiciously, making sure to +carefully consider whether the interaction of the encode/decode/mm_field is consistent with what you expect! + + +#### What if I have other dataclass field extensions that rely on `metadata` + +All the `dataclasses_json.config` does is return a mapping, namespaced under the key `'dataclasses_json'`. + +Say there's another module, `other_dataclass_package` that uses metadata. Here's how you solve your problem: + +```python +metadata = {'other_dataclass_package': 'some metadata...'} # pre-existing metadata for another dataclass package +dataclass_json_config = config( + encoder=datetime.isoformat, + decoder=datetime.fromisoformat, + mm_field=fields.DateTime(format='iso') + ) +metadata.update(dataclass_json_config) + +@dataclass_json +@dataclass +class DataClassWithIsoDatetime: + created_at: datetime = field(metadata=metadata) +``` + +You can also manually specify the dataclass_json configuration mapping. + +```python +@dataclass_json +@dataclass +class DataClassWithIsoDatetime: + created_at: date = field( + metadata={'dataclasses_json': { + 'encoder': date.isoformat, + 'decoder': date.fromisoformat, + 'mm_field': fields.DateTime(format='iso') + }} + ) +``` + +## A larger example + +```python +from dataclasses import dataclass +from dataclasses_json import dataclass_json + +from typing import List + +@dataclass_json +@dataclass(frozen=True) +class Minion: + name: str + + +@dataclass_json +@dataclass(frozen=True) +class Boss: + minions: List[Minion] + +boss = Boss([Minion('evil minion'), Minion('very evil minion')]) +boss_json = """ +{ + "minions": [ + { + "name": "evil minion" + }, + { + "name": "very evil minion" + } + ] +} +""".strip() + +assert boss.to_json(indent=4) == boss_json +assert Boss.from_json(boss_json) == boss +``` + +## Performance + +Take a look at [this issue](https://github.com/lidatong/dataclasses-json/issues/228) + +## Versioning + +Note this library is still pre-1.0.0 (SEMVER). + +The current convention is: +- **PATCH** version upgrades for bug fixes and minor feature additions. +- **MINOR** version upgrades for big API features and breaking changes. + +Once this library is 1.0.0, it will follow standard SEMVER conventions. + + +## Roadmap + +Currently the focus is on investigating and fixing bugs in this library, working +on performance, and finishing [this issue](https://github.com/lidatong/dataclasses-json/issues/31). + +That said, if you think there's a feature missing / something new needed in the +library, please see the contributing section below. + + +## Contributing + +First of all, thank you for being interested in contributing to this library. +I really appreciate you taking the time to work on this project. + +- If you're just interested in getting into the code, a good place to start are +issues tagged as bugs. +- If introducing a new feature, especially one that modifies the public API, +consider submitting an issue for discussion before a PR. Please also take a look +at existing issues / PRs to see what you're proposing has already been covered +before / exists. +- I like to follow the commit conventions documented [here](https://www.conventionalcommits.org/en/v1.0.0/#summary) + + + + +%package help +Summary: Development documents and examples for dataclasses-json +Provides: python3-dataclasses-json-doc +%description help +# Dataclasses JSON + + + +This library provides a simple API for encoding and decoding [dataclasses](https://docs.python.org/3/library/dataclasses.html) to and from JSON. + +It's very easy to get started. + +[README / Documentation website](https://lidatong.github.io/dataclasses-json). Features a navigation bar and search functionality, and should mirror this README exactly -- take a look! + +## Quickstart + +`pip install dataclasses-json` + +```python +from dataclasses import dataclass +from dataclasses_json import dataclass_json + + +@dataclass_json +@dataclass +class Person: + name: str + + +person = Person(name='lidatong') +person.to_json() # '{"name": "lidatong"}' <- this is a string +person.to_dict() # {'name': 'lidatong'} <- this is a dict +Person.from_json('{"name": "lidatong"}') # Person(1) +Person.from_dict({'name': 'lidatong'}) # Person(1) + +# You can also apply _schema validation_ using an alternative API +# This can be useful for "typed" Python code + +Person.from_json('{"name": 42}') # This is ok. 42 is not a `str`, but + # dataclass creation does not validate types +Person.schema().loads('{"name": 42}') # Error! Raises `ValidationError` +``` + +**What if you want to work with camelCase JSON?** + +```python +# same imports as above, with the additional `LetterCase` import +from dataclasses import dataclass +from dataclasses_json import dataclass_json, LetterCase + +@dataclass_json(letter_case=LetterCase.CAMEL) # now all fields are encoded/decoded from camelCase +@dataclass +class ConfiguredSimpleExample: + int_field: int + +ConfiguredSimpleExample(1).to_json() # {"intField": 1} +ConfiguredSimpleExample.from_json('{"intField": 1}') # ConfiguredSimpleExample(1) +``` + +## Supported types + +It's recursive (see caveats below), so you can easily work with nested dataclasses. +In addition to the supported types in the +[py to JSON table](https://docs.python.org/3/library/json.html#py-to-json-table), this library supports the following: + +- any arbitrary [Collection](https://docs.python.org/3/library/collections.abc.html#collections.abc.Collection) type is supported. +[Mapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping) types are encoded as JSON objects and `str` types as JSON strings. +Any other Collection types are encoded into JSON arrays, but decoded into the original collection types. + +- [datetime](https://docs.python.org/3/library/datetime.html#available-types) +objects. `datetime` objects are encoded to `float` (JSON number) using +[timestamp](https://docs.python.org/3/library/datetime.html#datetime.datetime.timestamp). +As specified in the `datetime` docs, if your `datetime` object is naive, it will +assume your system local timezone when calling `.timestamp()`. JSON numbers +corresponding to a `datetime` field in your dataclass are decoded +into a datetime-aware object, with `tzinfo` set to your system local timezone. +Thus, if you encode a datetime-naive object, you will decode into a +datetime-aware object. This is important, because encoding and decoding won't +strictly be inverses. See [this section](#Overriding) if you want to override this default +behavior (for example, if you want to use ISO). + +- [UUID](https://docs.python.org/3/library/uuid.html#uuid.UUID) objects. They +are encoded as `str` (JSON string). + +- [Decimal](https://docs.python.org/3/library/decimal.html) objects. They are +also encoded as `str`. + +**The [latest release](https://github.com/lidatong/dataclasses-json/releases/latest) is compatible with both Python 3.7 and Python 3.6 (with the dataclasses backport).** + +## Usage + +#### Approach 1: Class decorator + +```python +from dataclasses import dataclass +from dataclasses_json import dataclass_json + +@dataclass_json +@dataclass +class Person: + name: str + +lidatong = Person('lidatong') + +# Encoding to JSON +lidatong.to_json() # '{"name": "lidatong"}' + +# Decoding from JSON +Person.from_json('{"name": "lidatong"}') # Person(name='lidatong') +``` + +Note that the `@dataclass_json` decorator must be stacked above the `@dataclass` +decorator (order matters!) + +#### Approach 2: Inherit from a mixin + +```python +from dataclasses import dataclass +from dataclasses_json import DataClassJsonMixin + +@dataclass +class Person(DataClassJsonMixin): + name: str + +lidatong = Person('lidatong') + +# A different example from Approach 1 above, but usage is the exact same +assert Person.from_json(lidatong.to_json()) == lidatong +``` + +Pick whichever approach suits your taste. Note that there is better support for + the mixin approach when using _static analysis_ tools (e.g. linting, typing), + but the differences in implementation will be invisible in _runtime_ usage. + +## How do I... + + + +### Use my dataclass with JSON arrays or objects? + +```python +from dataclasses import dataclass +from dataclasses_json import dataclass_json + +@dataclass_json +@dataclass +class Person: + name: str +``` + +**Encode into a JSON array containing instances of my Data Class** + +```python +people_json = [Person('lidatong')] +Person.schema().dumps(people_json, many=True) # '[{"name": "lidatong"}]' +``` + +**Decode a JSON array containing instances of my Data Class** + +```python +people_json = '[{"name": "lidatong"}]' +Person.schema().loads(people_json, many=True) # [Person(name='lidatong')] +``` + +**Encode as part of a larger JSON object containing my Data Class (e.g. an HTTP +request/response)** + +```python +import json + +response_dict = { + 'response': { + 'person': Person('lidatong').to_dict() + } +} + +response_json = json.dumps(response_dict) +``` + +In this case, we do two steps. First, we encode the dataclass into a +**python dictionary** rather than a JSON string, using `.to_dict`. + +Second, we leverage the built-in `json.dumps` to serialize our `dataclass` into +a JSON string. + +**Decode as part of a larger JSON object containing my Data Class (e.g. an HTTP +response)** + +```python +import json + +response_dict = json.loads('{"response": {"person": {"name": "lidatong"}}}') + +person_dict = response_dict['response'] + +person = Person.from_dict(person_dict) +``` + +In a similar vein to encoding above, we leverage the built-in `json` module. + +First, call `json.loads` to read the entire JSON object into a +dictionary. We then access the key of the value containing the encoded dict of +our `Person` that we want to decode (`response_dict['response']`). + +Second, we load in the dictionary using `Person.from_dict`. + + +### Encode or decode into Python lists/dictionaries rather than JSON? + +This can be by calling `.schema()` and then using the corresponding +encoder/decoder methods, ie. `.load(...)`/`.dump(...)`. + +**Encode into a single Python dictionary** + +```python +person = Person('lidatong') +person.to_dict() # {'name': 'lidatong'} +``` + +**Encode into a list of Python dictionaries** + +```python +people = [Person('lidatong')] +Person.schema().dump(people, many=True) # [{'name': 'lidatong'}] +``` + +**Decode a dictionary into a single dataclass instance** + +```python +person_dict = {'name': 'lidatong'} +Person.from_dict(person_dict) # Person(name='lidatong') +``` + +**Decode a list of dictionaries into a list of dataclass instances** + +```python +people_dicts = [{"name": "lidatong"}] +Person.schema().load(people_dicts, many=True) # [Person(name='lidatong')] +``` + +### Encode or decode from camelCase (or kebab-case)? + +JSON letter case by convention is camelCase, in Python members are by convention snake_case. + +You can configure it to encode/decode from other casing schemes at both the class level and the field level. + +```python +from dataclasses import dataclass, field + +from dataclasses_json import LetterCase, config, dataclass_json + + +# changing casing at the class level +@dataclass_json(letter_case=LetterCase.CAMEL) +@dataclass +class Person: + given_name: str + family_name: str + +Person('Alice', 'Liddell').to_json() # '{"givenName": "Alice"}' +Person.from_json('{"givenName": "Alice", "familyName": "Liddell"}') # Person('Alice', 'Liddell') + +# at the field level +@dataclass_json +@dataclass +class Person: + given_name: str = field(metadata=config(letter_case=LetterCase.CAMEL)) + family_name: str + +Person('Alice', 'Liddell').to_json() # '{"givenName": "Alice"}' +# notice how the `family_name` field is still snake_case, because it wasn't configured above +Person.from_json('{"givenName": "Alice", "family_name": "Liddell"}') # Person('Alice', 'Liddell') +``` + +**This library assumes your field follows the Python convention of snake_case naming.** +If your field is not `snake_case` to begin with and you attempt to parameterize `LetterCase`, +the behavior of encoding/decoding is undefined (most likely it will result in subtle bugs). + +### Encode or decode using a different name + +```python +from dataclasses import dataclass, field + +from dataclasses_json import config, dataclass_json + +@dataclass_json +@dataclass +class Person: + given_name: str = field(metadata=config(field_name="overriddenGivenName")) + +Person(given_name="Alice") # Person('Alice') +Person.from_json('{"overriddenGivenName": "Alice"}') # Person('Alice') +Person('Alice').to_json() # {"overriddenGivenName": "Alice"} +``` + +### Handle missing or optional field values when decoding? + +By default, any fields in your dataclass that use `default` or +`default_factory` will have the values filled with the provided default, if the +corresponding field is missing from the JSON you're decoding. + +**Decode JSON with missing field** + +```python +@dataclass_json +@dataclass +class Student: + id: int + name: str = 'student' + +Student.from_json('{"id": 1}') # Student(id=1, name='student') +``` + +Notice `from_json` filled the field `name` with the specified default 'student' +when it was missing from the JSON. + +Sometimes you have fields that are typed as `Optional`, but you don't +necessarily want to assign a default. In that case, you can use the +`infer_missing` kwarg to make `from_json` infer the missing field value as `None`. + +**Decode optional field without default** + +```python +@dataclass_json +@dataclass +class Tutor: + id: int + student: Optional[Student] = None + +Tutor.from_json('{"id": 1}') # Tutor(id=1, student=None) +``` + +Personally I recommend you leverage dataclass defaults rather than using +`infer_missing`, but if for some reason you need to decouple the behavior of +JSON decoding from the field's default value, this will allow you to do so. + + +### Handle unknown / extraneous fields in JSON? + +By default, it is up to the implementation what happens when a `json_dataclass` receives input parameters that are not defined. +(the `from_dict` method ignores them, when loading using `schema()` a ValidationError is raised.) +There are three ways to customize this behavior. + +Assume you want to instantiate a dataclass with the following dictionary: +```python +dump_dict = {"endpoint": "some_api_endpoint", "data": {"foo": 1, "bar": "2"}, "undefined_field_name": [1, 2, 3]} +``` + +1. You can enforce to always raise an error by setting the `undefined` keyword to `Undefined.RAISE` + (`'RAISE'` as a case-insensitive string works as well). Of course it works normally if you don't pass any undefined parameters. + +```python +from dataclasses_json import Undefined + +@dataclass_json(undefined=Undefined.RAISE) +@dataclass() +class ExactAPIDump: + endpoint: str + data: Dict[str, Any] + +dump = ExactAPIDump.from_dict(dump_dict) # raises UndefinedParameterError +``` + +2. You can simply ignore any undefined parameters by setting the `undefined` keyword to `Undefined.EXCLUDE` + (`'EXCLUDE'` as a case-insensitive string works as well). Note that you will not be able to retrieve them using `to_dict`: + +```python +from dataclasses_json import Undefined + +@dataclass_json(undefined=Undefined.EXCLUDE) +@dataclass() +class DontCareAPIDump: + endpoint: str + data: Dict[str, Any] + +dump = DontCareAPIDump.from_dict(dump_dict) # DontCareAPIDump(endpoint='some_api_endpoint', data={'foo': 1, 'bar': '2'}) +dump.to_dict() # {"endpoint": "some_api_endpoint", "data": {"foo": 1, "bar": "2"}} +``` + +3. You can save them in a catch-all field and do whatever needs to be done later. Simply set the `undefined` +keyword to `Undefined.INCLUDE` (`'INCLUDE'` as a case-insensitive string works as well) and define a field +of type `CatchAll` where all unknown values will end up. + This simply represents a dictionary that can hold anything. + If there are no undefined parameters, this will be an empty dictionary. + +```python +from dataclasses_json import Undefined, CatchAll + +@dataclass_json(undefined=Undefined.INCLUDE) +@dataclass() +class UnknownAPIDump: + endpoint: str + data: Dict[str, Any] + unknown_things: CatchAll + +dump = UnknownAPIDump.from_dict(dump_dict) # UnknownAPIDump(endpoint='some_api_endpoint', data={'foo': 1, 'bar': '2'}, unknown_things={'undefined_field_name': [1, 2, 3]}) +dump.to_dict() # {'endpoint': 'some_api_endpoint', 'data': {'foo': 1, 'bar': '2'}, 'undefined_field_name': [1, 2, 3]} +``` + +Notes: +- When using `Undefined.INCLUDE`, an `UndefinedParameterError` will be raised if you don't specify +exactly one field of type `CatchAll`. +- Note that `LetterCase` does not affect values written into the `CatchAll` field, they will be as they are given. +- When specifying a default (or a default factory) for the the `CatchAll`-field, e.g. `unknown_things: CatchAll = None`, the default value will be used instead of an empty dict if there are no undefined parameters. +- Calling __init__ with non-keyword arguments resolves the arguments to the defined fields and writes everything else into the catch-all field. + +4. All 3 options work as well using `schema().loads` and `schema().dumps`, as long as you don't overwrite it by specifying `schema(unknown=<a marshmallow value>)`. +marshmallow uses the same 3 keywords ['include', 'exclude', 'raise'](https://marshmallow.readthedocs.io/en/stable/quickstart.html#handling-unknown-fields). + +5. All 3 operations work as well using `__init__`, e.g. `UnknownAPIDump(**dump_dict)` will **not** raise a `TypeError`, but write all unknown values to the field tagged as `CatchAll`. + Classes tagged with `EXCLUDE` will also simply ignore unknown parameters. Note that classes tagged as `RAISE` still raise a `TypeError`, and **not** a `UndefinedParameterError` if supplied with unknown keywords. + + +### Override the default encode / decode / marshmallow field of a specific field? + +See [Overriding](#Overriding) + +### Handle recursive dataclasses? +Object hierarchies where fields are of the type that they are declared within require a small +type hinting trick to declare the forward reference. +```python +from typing import Optional +from dataclasses import dataclass +from dataclasses_json import dataclass_json + +@dataclass_json +@dataclass +class Tree(): + value: str + left: Optional['Tree'] + right: Optional['Tree'] +``` + +Avoid using +```python +from __future__ import annotations +``` +as it will cause problems with the way dataclasses_json accesses the type annotations. + + +## Marshmallow interop + +Using the `dataclass_json` decorator or mixing in `DataClassJsonMixin` will +provide you with an additional method `.schema()`. + +`.schema()` generates a schema exactly equivalent to manually creating a +marshmallow schema for your dataclass. You can reference the [marshmallow API docs](https://marshmallow.readthedocs.io/en/3.0/api_reference.html#schema) +to learn other ways you can use the schema returned by `.schema()`. + +You can pass in the exact same arguments to `.schema()` that you would when +constructing a `PersonSchema` instance, e.g. `.schema(many=True)`, and they will +get passed through to the marshmallow schema. + + +```python +from dataclasses import dataclass +from dataclasses_json import dataclass_json + +@dataclass_json +@dataclass +class Person: + name: str + +# You don't need to do this - it's generated for you by `.schema()`! +from marshmallow import Schema, fields + +class PersonSchema(Schema): + name = fields.Str() +``` + +Briefly, on what's going on under the hood in the above examples: calling +`.schema()` will have this library generate a +[marshmallow schema]('https://marshmallow.readthedocs.io/en/3.0/api_reference.html#schema) +for you. It also fills in the corresponding object hook, so that marshmallow +will create an instance of your Data Class on `load` (e.g. +`Person.schema().load` returns a `Person`) rather than a `dict`, which it does +by default in marshmallow. + +**Performance note** + +`.schema()` is not cached (it generates the schema on every call), so if you +have a nested Data Class you may want to save the result to a variable to +avoid re-generation of the schema on every usage. + +```python +person_schema = Person.schema() +person_schema.dump(people, many=True) + +# later in the code... + +person_schema.dump(person) +``` + +## Overriding / Extending + +#### Overriding + +For example, you might want to encode/decode `datetime` objects using ISO format +rather than the default `timestamp`. + +```python +from dataclasses import dataclass, field +from dataclasses_json import dataclass_json, config +from datetime import datetime +from marshmallow import fields + +@dataclass_json +@dataclass +class DataClassWithIsoDatetime: + created_at: datetime = field( + metadata=config( + encoder=datetime.isoformat, + decoder=datetime.fromisoformat, + mm_field=fields.DateTime(format='iso') + ) + ) +``` + +#### Extending + +Similarly, you might want to extend `dataclasses_json` to encode `date` objects. + +```python +from dataclasses import dataclass, field +from dataclasses_json import dataclass_json, config +from datetime import date +from marshmallow import fields + +@dataclass_json +@dataclass +class DataClassWithIsoDatetime: + created_at: date = field( + metadata=config( + encoder= date.isoformat, + decoder= date.fromisoformat, + mm_field= fields.DateTime(format='iso') + )) +``` + +As you can see, you can **override** or **extend** the default codecs by providing a "hook" via a +callable: +- `encoder`: a callable, which will be invoked to convert the field value when encoding to JSON +- `decoder`: a callable, which will be invoked to convert the JSON value when decoding from JSON +- `mm_field`: a marshmallow field, which will affect the behavior of any operations involving `.schema()` + +Note that these hooks will be invoked regardless if you're using +`.to_json`/`dump`/`dumps` +and `.from_json`/`load`/`loads`. So apply overrides / extensions judiciously, making sure to +carefully consider whether the interaction of the encode/decode/mm_field is consistent with what you expect! + + +#### What if I have other dataclass field extensions that rely on `metadata` + +All the `dataclasses_json.config` does is return a mapping, namespaced under the key `'dataclasses_json'`. + +Say there's another module, `other_dataclass_package` that uses metadata. Here's how you solve your problem: + +```python +metadata = {'other_dataclass_package': 'some metadata...'} # pre-existing metadata for another dataclass package +dataclass_json_config = config( + encoder=datetime.isoformat, + decoder=datetime.fromisoformat, + mm_field=fields.DateTime(format='iso') + ) +metadata.update(dataclass_json_config) + +@dataclass_json +@dataclass +class DataClassWithIsoDatetime: + created_at: datetime = field(metadata=metadata) +``` + +You can also manually specify the dataclass_json configuration mapping. + +```python +@dataclass_json +@dataclass +class DataClassWithIsoDatetime: + created_at: date = field( + metadata={'dataclasses_json': { + 'encoder': date.isoformat, + 'decoder': date.fromisoformat, + 'mm_field': fields.DateTime(format='iso') + }} + ) +``` + +## A larger example + +```python +from dataclasses import dataclass +from dataclasses_json import dataclass_json + +from typing import List + +@dataclass_json +@dataclass(frozen=True) +class Minion: + name: str + + +@dataclass_json +@dataclass(frozen=True) +class Boss: + minions: List[Minion] + +boss = Boss([Minion('evil minion'), Minion('very evil minion')]) +boss_json = """ +{ + "minions": [ + { + "name": "evil minion" + }, + { + "name": "very evil minion" + } + ] +} +""".strip() + +assert boss.to_json(indent=4) == boss_json +assert Boss.from_json(boss_json) == boss +``` + +## Performance + +Take a look at [this issue](https://github.com/lidatong/dataclasses-json/issues/228) + +## Versioning + +Note this library is still pre-1.0.0 (SEMVER). + +The current convention is: +- **PATCH** version upgrades for bug fixes and minor feature additions. +- **MINOR** version upgrades for big API features and breaking changes. + +Once this library is 1.0.0, it will follow standard SEMVER conventions. + + +## Roadmap + +Currently the focus is on investigating and fixing bugs in this library, working +on performance, and finishing [this issue](https://github.com/lidatong/dataclasses-json/issues/31). + +That said, if you think there's a feature missing / something new needed in the +library, please see the contributing section below. + + +## Contributing + +First of all, thank you for being interested in contributing to this library. +I really appreciate you taking the time to work on this project. + +- If you're just interested in getting into the code, a good place to start are +issues tagged as bugs. +- If introducing a new feature, especially one that modifies the public API, +consider submitting an issue for discussion before a PR. Please also take a look +at existing issues / PRs to see what you're proposing has already been covered +before / exists. +- I like to follow the commit conventions documented [here](https://www.conventionalcommits.org/en/v1.0.0/#summary) + + + + +%prep +%autosetup -n dataclasses-json-0.5.7 + +%build +%py3_build + +%install +%py3_install +install -d -m755 %{buildroot}/%{_pkgdocdir} +if [ -d doc ]; then cp -arf doc %{buildroot}/%{_pkgdocdir}; fi +if [ -d docs ]; then cp -arf docs %{buildroot}/%{_pkgdocdir}; fi +if [ -d example ]; then cp -arf example %{buildroot}/%{_pkgdocdir}; fi +if [ -d examples ]; then cp -arf examples %{buildroot}/%{_pkgdocdir}; fi +pushd %{buildroot} +if [ -d usr/lib ]; then + find usr/lib -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/lib64 ]; then + find usr/lib64 -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/bin ]; then + find usr/bin -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/sbin ]; then + find usr/sbin -type f -printf "/%h/%f\n" >> filelist.lst +fi +touch doclist.lst +if [ -d usr/share/man ]; then + find usr/share/man -type f -printf "/%h/%f.gz\n" >> doclist.lst +fi +popd +mv %{buildroot}/filelist.lst . +mv %{buildroot}/doclist.lst . + +%files -n python3-dataclasses-json -f filelist.lst +%dir %{python3_sitelib}/* + +%files help -f doclist.lst +%{_docdir}/* + +%changelog +* Mon Apr 10 2023 Python_Bot <Python_Bot@openeuler.org> - 0.5.7-1 +- Package Spec generated @@ -0,0 +1 @@ +f5016577e2d411c31338543a2c7b1dab dataclasses-json-0.5.7.tar.gz |