Skip to main content

Simple and flexible conversions between dataclasses and jsonable dictionaries.

Project description

dataclass-jsonable

dataclass-jsonable ci

Simple and flexible conversions between dataclasses and jsonable dictionaries.

It maps dataclasses to jsonable dictionaries but not json strings.

Features

  • Easy to use.
  • Supports common type annotations.
  • Supports recursive conversions.
  • Supports field-level and dataclass-level overriding.

Installation

Requirements: Python >= 3.7

Install via pip:

pip install dataclass-jsonable

Quick Example

from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
from enum import IntEnum
from typing import List
from dataclass_jsonable import J

class Color(IntEnum):
    BLACK = 0
    BLUE = 1
    RED = 2

@dataclass
class Pen(J):
    color: Color
    price: Decimal
    produced_at: datetime

@dataclass
class Box(J):
    pens: List[Pen]

box = Box(pens=[Pen(color=Color.BLUE, price=Decimal("20.1"), produced_at=datetime.now())])

# Encode to a jsonable dictionary.
d = box.json()
print(d)  # {'pens': [{'color': 1, 'price': '20.1', 'produced_at': 1660023062}]}

# Construct dataclass from a jsonable dictionary.
print(Box.from_json(d))

APIs are only the two: .json() and .from_json().

Built-in Supported Types

  • bool, int, float, str, None encoded as it is.

    @dataclass
    class Obj(J):
        a: int
        b: str
        c: bool
        d: None
    
    Obj(a=1, b="b", c=True, d=None).json()
    # => {'a': 1, 'b': 'b', 'c': True, 'd': None}
    
  • Decimal encoded to str.

    @dataclass
    class Obj(J):
        a: Decimal
    
    Obj(a=Decimal("3.1")).json()  # => {'a': '3.1'}
    
  • datetime encoded to timestamp integer via .timestamp() method. timedelta encoded to integer via .total_seconds() method.

    @dataclass
    class Obj(J):
        a: datetime
        b: timedelta
    
    Obj(a=datetime.now(), b=timedelta(minutes=1)).json()
    # => {'a': 1660062019, 'b': 60}
    
  • Enum and IntEnum encoded to their values via .value attribute.

    @dataclass
    class Obj(J):
        status: Status
    
    Obj(status=Status.DONE).json()  # => {'status': 1}
    
  • Any is encoded according to its type.

    @dataclass
    class Obj(J):
        a: Any
    
    Obj(1).json()  # {'a': 1}
    Obj("a").json()  # {'a': 'a'}
    Obj.from_json({"a": 1})  # Obj(a=1)
    
  • Optional[X] is supported, but Union[X, Y, ...] is not.

    @dataclass
    class Obj(J):
        a: Optional[int] = None
    
    Obj(a=1).json()  # => {'a': 1}
    
  • List[X], Tuple[X], Set[X] are all encoded to list.

    @dataclass
    class Obj(J):
        a: List[int]
        b: Set[int]
        c: Tuple[int, str]
        d: Tuple[int, ...]
    
    Obj(a=[1], b={2, 3}, c=(4, "5"), d=(7, 8, 9)).json())
    # => {'a': [1], 'b': [2, 3], 'c': [4, '5'], 'd': [7, 8, 9]}
    
    Obj.from_json({"a": [1], "b": [2, 3], "c": [4, "5"], "d": [7, 8, 9]}))
    # => Obj(a=[1], b={2, 3}, c=(4, '5'), d=(7, 8, 9))
    
  • Dict[str, X] encoded to dict.

    @dataclass
    class Obj(J):
        a: Dict[str, int]
    Obj(a={"x": 1}).json()  # => {'a': {'x': 1}}
    Obj.from_json({"a": {"x": 1}}) # => Obj(a={'x': 1})
    
  • Nested or recursively JSONAble (or J) dataclasses.

    @dataclass
    class Elem(J):
        k: str
    
    @dataclass
    class Obj(J):
        a: List[Elem]
    
    Obj([Elem("v")]).json()  # => {'a': [{'k': 'v'}]}
    Obj.from_json({"a": [{"k": "v"}]})  # Obj(a=[Elem(k='v')])
    
  • Postponed annotations (the ForwardRef in PEP 563).

    @dataclass
    class Node(J):
        name: str
        left: Optional["Node"] = None
        right: Optional["Node"] = None
    
    root = Node("root", left=Node("left"), right=Node("right"))
    root.json()
    # {'name': 'root', 'left': {'name': 'left', 'left': None, 'right': None}, 'right': {'name': 'right', 'left': None, 'right': None}}
    

If these built-in default conversion behaviors do not meet your needs, or your type is not on the list, you can use json_options introduced below to customize it.

Customization / Overriding Examples

We can override the default conversion behaviors with json_options, which uses the dataclass field's metadata for field-level customization purpose, and the namespace is j.

The following pseudo code gives the pattern:

from dataclasses import field
from dataclass_jsonable import json_options

@dataclass
class Struct(J):
    attr: T = field(metadata={"j": json_options(**kwds)})

An example list about json_options:

  • Specific a custom dictionary key over the default field's name:

    @dataclass
    class Person(J):
        attr: str = field(metadata={"j": json_options(name="new_attr")})
    Person(attr="value").json() # => {"new_attr": "value"}
    
  • Omit a field if its value is empty:

    @dataclass
    class Book(J):
        name: str = field(metadata={"j": json_options(omitempty=True)})
    Book(name="").json() # => {}
    

    Further, we can specify what is 'empty' via option omitempty_tester:

    @dataclass
    class Book(J):
        attr: Optional[str] = field(
            default=None,
            metadata={
                # By default, we test `empty` using `not x`.
                "j": json_options(omitempty=True, omitempty_tester=lambda x: x is None)
            },
        )
    
    Book(attr="").json()  # => {'attr': ''}
    Book(attr=None).json()  # => {}
    
  • Always skip a field. So we can stop some "private" fields from exporting:

    @dataclass
    class Obj(J):
        attr: str = field(metadata={"j": json_options(skip=True)})
    
    Obj(attr="private").json() # => {}
    
  • dataclasses's field allows us to pass a default or default_factory argument to set a default value:

    @dataclass
    class Obj(J):
        attr: List[str] = field(default_factory=list, metadata={"j": json_options(**kwds)})
    

    There's also an option default_on_missing in dataclass-jsonable, which specifics a default value before decoding if the key is missing in the dictionary. Sometimes this way is more concise:

    @dataclass
    class Obj(J):
        updated_at: datetime = field(metadata={"j": json_options(default_on_missing=0)})
    
    Obj.from_json({})  # => Obj(updated_at=datetime.datetime(1970, 1, 1, 8, 0))
    
  • Override the default encoders and decoders.

    This way, you have complete control over how to encode and decode at field level.

    @dataclass
    class Obj(J):
        elems: List[str] = field(
            metadata={
                "j": json_options(
                    encoder=lambda x: ",".join(x),
                    decoder=lambda x: x.split(","),
                )
            }
        )
    
    Obj(elems=["a", "b", "c"]).json()  # => {'elems': 'a,b,c'}
    Obj.from_json({"elems": "a,b,c"})  # => Obj(elems=['a', 'b', 'c'])
    

    The following code snippet about datetime is a very common example, you might want ISO format datetime conversion over timestamp integers.

    @dataclass
    class Record(J):
        created_at: datetime = field(
            default_factory=datetime.now,
            metadata={
                "j": json_options(
                    encoder=datetime.isoformat,
                    decoder=datetime.fromisoformat,
                )
            },
        )
    
    Record().json()  # => {'created_at': '2022-08-09T23:23:02.543007'}
    
  • For some very narrow scenarios, we may need to execute a hook function before decoding, for example, the data to be decoded is a serialized json string, and but we still want to use the built-in decoder functions instead of making a new decoder.

    import json
    
    @dataclass
    class Obj(J):
        data: Dict[str, Any] = field(metadata={"j": json_options(before_decoder=json.loads)})
    
    Obj.from_json({"data": '{"k": "v"}'})
    # => Obj(data={'k': 'v'})
    
  • Customize default behaviors at the class level.

    If an option is not explicitly set at the field level, the __default_json_options__ provided at the class level will be attempted.

    @dataclass
    class Obj(J):
        __default_json_options__ = json_options(omitempty=True)
    
        a: Optional[int] = None
        b: Optional[str] = None
    
    Obj(b="b").json() # => {'b': 'b'}
    

License

BSD.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dataclass-jsonable-0.0.7.tar.gz (9.1 kB view details)

Uploaded Source

File details

Details for the file dataclass-jsonable-0.0.7.tar.gz.

File metadata

  • Download URL: dataclass-jsonable-0.0.7.tar.gz
  • Upload date:
  • Size: 9.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.1 CPython/3.9.6

File hashes

Hashes for dataclass-jsonable-0.0.7.tar.gz
Algorithm Hash digest
SHA256 14201163984ef7567fe68ff61dc0d0985ba9a0308cf809e064cdec495549c729
MD5 d3ce42e09655217dedffc1339341e9d1
BLAKE2b-256 e82b51da1bfe3f366be1c1d629b4b37dc62b1275d4fb1d8cbfb7a70c26d57227

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page