{"id":18296968,"url":"https://github.com/davidcellis/ducktools-jsonkit","last_synced_at":"2026-02-22T12:46:27.344Z","repository":{"id":64972428,"uuid":"580188832","full_name":"DavidCEllis/ducktools-jsonkit","owner":"DavidCEllis","description":"Default functions and function makers for JSON serialization of python objects.","archived":false,"fork":false,"pushed_at":"2024-06-07T17:24:06.000Z","size":93,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-01T04:34:54.923Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DavidCEllis.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-12-19T23:48:29.000Z","updated_at":"2024-06-07T17:24:10.000Z","dependencies_parsed_at":"2023-12-24T12:32:58.885Z","dependency_job_id":"9839bdf9-515b-457c-a051-59889f723d5b","html_url":"https://github.com/DavidCEllis/ducktools-jsonkit","commit_stats":null,"previous_names":["davidcellis/ducktools-jsonkit"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavidCEllis%2Fducktools-jsonkit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavidCEllis%2Fducktools-jsonkit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavidCEllis%2Fducktools-jsonkit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavidCEllis%2Fducktools-jsonkit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DavidCEllis","download_url":"https://codeload.github.com/DavidCEllis/ducktools-jsonkit/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245735886,"owners_count":20663807,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-05T14:45:05.574Z","updated_at":"2025-10-24T07:09:19.191Z","avatar_url":"https://github.com/DavidCEllis.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ducktools: jsonkit #\n\nDefault functions and default function generators to make JSON serialization\nwith the python standard library easier.\n\n## Motivation ##\n\nThe documentation for the JSON module in the Python standard library (as of 3.11.1)\ninstructs the user to subclass `JSONEncoder` if you wish to serialize objects\nthat are not natively serializable. This is unnecessary. The serialization methods\n`dump` and `dumps` provide a `default` argument which achieves the same result\nwithout needing to subclass.\n\nThis module provides some functions and function generators that can be used as\nvalues for this `default` argument to serialize some standard classes and custom\nclasses.\n\nUnlike `JSONEncoder` subclasses, `default` functions are also supported as arguments\nin some other libraries that implement their own JSON serialization such as\n[orjson](https://github.com/ijl/orjson) or\n[rapidjson](https://github.com/python-rapidjson/python-rapidjson).\n\nIf you're using the `encode` method on a `JSONEncoder` class directly you can provide\nthe `default` function as an argument to `JSONEncoder` in the same way as to `dumps`.\nIf `dumps` is being called multiple times with a default, creating a `JSONEncoder` instance\nand calling the `encode` method directly will be faster as `dumps` creates a new instance\neach time it is called.\n\n## Generated methods for field and dataclass serialization ##\n\nThe serializers for dataclasses and fields exist for cases where you need to \nencode a large number of instances of the same dataclass (or other objects \nwith the same set of fields).\n\nWhile calling exec usually takes longer than a single naive serialization, \nthe resulting static functions are faster than their dynamic equivalents. \nThis is noticeable when serializing a large number of instances of the same class. \nAs the results are cached, the cost of `exec` is only paid the first time.\n\nThis is actually similar to the method\n[cattrs](https://github.com/python-attrs/cattrs)\nuses, although that module uses `eval(compile(...))` to provide a 'fake' source\nfile for inspections. If you're already using\n[attrs](https://github.com/python-attrs/attrs)\nyou should use `cattrs` for serialization.\n\n## Methods ##\n\nThe `method_default` function is provided to create a `default` function to pass\nto json.dumps if you have classes with a method that is intended to prepare\nthem for serialization.\n\nExample:\n\n```python\nimport json\nfrom ducktools.jsonkit import method_default\n\n\nclass Example:\n    def __init__(self, x, y):\n        self.x, self.y = x, y\n\n    def asdict(self):\n        return {'x': self.x, 'y': self.y}\n\n\nexample = Example(\"hello\", \"world\")\n\n# dumps\ndata = json.dumps(example, default=method_default('asdict'))\n\n# encoder\nencoder = json.JSONEncoder(default=method_default('asdict'))\nencoder_data = encoder.encode(example)\n\nprint(encoder_data == data)\nprint(data)\n```\n\nOutput:\n```\nTrue\n{\"x\": \"hello\", \"y\": \"world\"}\n```\n\n## Merge defaults ##\n\nThe `merge_defaults` function combines multiple `default` functions into one.\n\n```python\nimport json\nfrom pathlib import Path\nfrom ducktools.jsonkit import merge_defaults\n\n\ndef path_default(pth):\n    if isinstance(pth, Path):\n        return str(pth)\n    else:\n        raise TypeError()\n\n\ndef set_default(s):\n    if isinstance(s, set):\n        return list(s)\n    else:\n        raise TypeError()\n\n\nnew_default = merge_defaults(path_default, set_default)\n\ndata = {\"Path\": Path(\"usr/bin/python\"), \"versions\": {'3.9', '3.10', '3.11'}}\n\nprint(json.dumps(data, default=new_default))\n```\n\nOutput:\n```\n{\"Path\": \"usr/bin/python\", \"versions\": [\"3.11\", \"3.9\", \"3.10\"]}\n```\n\n## Register ##\n\nThe module provides a `JSONRegister` class that provides methods\nto add classes and their serialization methods to the register, these are\nthen used by providing the `JSONRegister` instance `default` to `json.dumps`.\n\n\u003e [!NOTE]\n\u003e The `register_method` decorator **does not work** on slotted dataclasses.\n\u003e @dataclass(slots=True) replaces the original class so instances of the new\n\u003e class are not instances of the original class stored as a reference in the\n\u003e decorator.\n\u003e \n\u003e Use `register.register(cls, cls.method)` for slotted dataclasses.\n\nExample:\n\n```python\nfrom ducktools.jsonkit import JSONRegister\n\nimport json\nimport dataclasses\nfrom pathlib import Path\nfrom decimal import Decimal\n\nregister = JSONRegister()\n\n\n@dataclasses.dataclass\nclass Demo:\n    id: int\n    name: str\n    location: Path\n    numbers: list[Decimal]\n\n    @register.register_method\n    def to_json(self):\n        return {\n            'id': self.id,\n            'name': self.name,\n            'location': self.location,\n            'numbers': self.numbers,\n        }\n\n\nregister.register(Path, str)\n\n\n@register.register_function(Decimal)\ndef unstructure_decimal(val):\n    return {'cls': 'Decimal', 'value': str(val)}\n\n\nnumbers = [Decimal(f\"{i}\") / Decimal('1000') for i in range(1, 3)]\npth = Path(\"usr/bin/python\")\n\ndemo = Demo(id=42, name=\"Demonstration Class\", location=pth, numbers=numbers)\n\nprint(json.dumps(demo, default=register.default, indent=2))\n```\n\nOutput:\n```\n{\n  \"id\": 42,\n  \"name\": \"Demonstration Class\",\n  \"location\": \"usr/bin/python\",\n  \"numbers\": [\n    {\n      \"cls\": \"Decimal\",\n      \"value\": \"0.001\"\n    },\n    {\n      \"cls\": \"Decimal\",\n      \"value\": \"0.002\"\n    }\n  ]\n}\n```\n\n## Fields ##\n\nThe `field_default` function is intended to be used to handle creating default for\nobjects where the serialization format is `{name: item.name, ...}`. This is used\nfor the dataclasses default provided.\n\nFor example this could be used to serialize classes based on the field names defined\nin `__slots__` (will not work on slots defined by a consumed iterable).\n\n```python\nimport json\nfrom functools import lru_cache\nfrom ducktools.jsonkit import field_default\n\n\n@lru_cache\ndef slot_defaultmaker(cls):\n    try:\n        slots = cls.__slots__\n    except AttributeError:\n        raise TypeError(f'Object of type {cls.__name__} is not JSON serializable')\n    slot_tuple = tuple(slots)\n    return field_default(slot_tuple)\n\n\ndef slot_default(o):\n    func = slot_defaultmaker(type(o))\n    return func(o)\n\n\nclass SlotExample:\n    __slots__ = ['x', 'y']\n\n    def __init__(self, x, y):\n        self.x, self.y = x, y\n\n\nexample = SlotExample(\"Hello\", \"World\")\n\ndata = json.dumps(example, default=slot_default)\nprint(data)\n```\n\nResult:\n```\n{\"x\": \"Hello\", \"y\": \"World\"}\n```\n\n## Dataclasses ##\n\nDataclasses itself provides its own `asdict` function, but unfortunately this\nincludes additional logic for deepcopying objects and performing recursive\nserialization.\n\nFor the purpose of basic serialization of dataclasses a basic non-recursive\ndefault method will be faster than `asdict`.\n\nNote: The `asdict` method has been improved in Python 3.12+ so the difference\nis less significant. \nSee [https://github.com/python/cpython/issues/103000](https://github.com/python/cpython/issues/103000).\n\n```python\nfrom dataclasses import is_dataclass, fields\ndef simple_dc_default(o):\n    if is_dataclass(o) and not isinstance(o, type):\n        return {f.name: getattr(o, f.name) for f in fields(o)}\n    else:\n        raise TypeError(\n            f'Object of type {type(o).__name__} is not JSON serializable'\n        )\n```\n\nUsing: performance/dataclass_serializers_compared.py\n\nComparing `asdict`, `simple_dc_default` (simple) and `dataclass_default` (cached).\n\nPython 3.11\n\n| Method           | Time /s | Time /cache |\n| ---------------- | ------- | ----------- |\n| json asdict      |  4.492  |    3.9 |\n| json simple      |  2.400  |    2.1 |\n| json cached      |  1.145  |    1.0 |\n\n\nPython 3.12\n\n| Method           | Time /s | Time /cache |\n| ---------------- | ------- | ----------- |\n| json asdict      |  1.991  |    2.2 |\n| json simple      |  1.910  |    2.1 |\n| json cached      |  0.896  |    1.0 |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidcellis%2Fducktools-jsonkit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdavidcellis%2Fducktools-jsonkit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidcellis%2Fducktools-jsonkit/lists"}