Skip to main content

Dict-Typer

A simple tool to take a json payload and convert it into Python TypedDict class definitions

A web version is available on https://pytyper.dev/

Why is this useful?

When dealing with API responses, you're very likely to be using JSON responses, and you might have deeply nested dictionaries, lists of items and it can be slightly hard to wrap your head around the structure of the responess you are working with. The first thing I usually do it try to create some data structure around it so that I can benefit from linting and typing information in my code.

Now this tends to be time consuming and error prone, so I thought it might be a good idea to automate this process with a tool for the future. Just as an example, if we take the output generated from the Example section below and imagine it's a response we get from an api. We can plug it in like this:

from project.types import Root


def get_from_api() -> Root:
    pass


def run() -> None:
    response = get_from_api()

    test1 = response["nested_dict"]["number"] + 1
    test2 = response["nested_dict"]["string"] + 1
    test3 = response["nested_dict"]["non_existant"] + 1
    for item in response["optional_items"]:
        print(item + 1)

and if we run mypy on this

-> % poetry run mypy test.py
test.py:43: error: Unsupported operand types for + ("str" and "int")
test.py:44: error: TypedDict "NestedDict" has no key 'non_existant'
test.py:46: error: Unsupported operand types for + ("None" and "int")
test.py:46: error: Unsupported operand types for + ("str" and "int")
test.py:46: note: Left operand is of type "Union[None, int, str]"
Found 4 errors in 1 file (checked 1 source file)

it will immediately detect four issues!

I also want to use this project to learn more about analysing code, making sure the project is well tested so that it's easy to experiment and try different approaches.

Usage

Either supply a path to a file or pipe json output to dict-typer

-> % dict-typer --help

Usage: dict-typer [OPTIONS] [FILE]...

Options:
  --imports / --no-imports  Show imports at the top, default: True
  -r, --rich                Show rich output.
  -l, --line-numbers        Show line numbers if rich.
  --version                 Show the version and exit.
  --help                    Show this message and exit.

-> % dict-typer ./.example.json
...

-> % curl example.com/test.json | dict-typer
...

TypeDict definitions

There are two ways to define a TypedDict, the primary one that uses the class based structure, as seen in the examples here. It's easier to read, but it has a limitation that the each key has to be avalid identifier and not a reserved keyword. Normally that's not an issue, but if you have for example, the following data

{
    "numeric-id": 123,
    "from": "far away",
}

which is valid json, but has the invalid identifier numeric-id and reserved keyword from, meaning the definition

class Root(TypedDict):
    numeric-id: int
    from: str

is invalid. In detected cases, dict-typer will use an alternative way to define those types, looking like this

Root = TypedDict('Root', {'numeric-id': int, 'from': str'})

which is not as readable, but valid.

dict-typer by default only uses the alternative definition for the types with invalid keys.

Lists

If the root of the payload is a list, it will be treated just like a list within a dictionary, where each item is parsed and definitions created for sub items. In these cases, a type alias is added as well to the output to capture the type of the list. For example, the list [1, "2", 3.0, { "id": 123 }, { "id": 456 }] will generate the following definitions:

from typing_extensions import TypedDict


class RootItem(TypedDict):
    id: int

Root = List[Union[RootItem, float, int, str]]

Examples

Calling from shell

-> % cat .example.json
{
  "number_int": 123,
  "number_float": 3.0,
  "string": "string",
  "list_single_type": ["a", "b", "c"],
  "list_mixed_type": ["1", 2, 3.0],
  "nested_dict": {
    "number": 1,
    "string": "value"
  },
  "same_nested_dict": {
    "number": 2,
    "string": "different value"
  },
  "multipe_levels": {
    "level2": {
      "level3": {
        "number": 3,
        "string": "more values"
      }
    }
  },
  "nested_invalid": { "numeric-id": 123, "from": "far away" },
  "optional_items": [1, 2, "3", "4", null, 5, 6, null]
}

-> % cat .example.json | dict-typer
from typing import List, Union

from typing_extensions import TypedDict


class NestedDict(TypedDict):
    number: int
    string: str


class Level2(TypedDict):
    level3: NestedDict


class MultipeLevels(TypedDict):
    level2: Level2


NestedInvalid = TypedDict("NestedInvalid", {
    "numeric-id": int,
    "from": str,
})


class Root(TypedDict):
    number_int: int
    number_float: float
    string: str
    list_single_type: List[str]
    list_mixed_type: List[Union[float, int, str]]
    nested_dict: NestedDict
    same_nested_dict: NestedDict
    multipe_levels: MultipeLevels
    nested_invalid: NestedInvalid
    optional_items: List[Union[None, int, str]]

Calling from Python

In [1]: source = {
   ...:   "number_int": 123,
   ...:   "number_float": 3.0,
   ...:   "string": "string",
   ...:   "list_single_type": ["a", "b", "c"],
   ...:   "list_mixed_type": ["1", 2, 3.0],
   ...:   "nested_dict": {
   ...:     "number": 1,
   ...:     "string": "value"
   ...:   },
   ...:   "same_nested_dict": {
   ...:     "number": 2,
   ...:     "string": "different value"
   ...:   },
   ...:   "multipe_levels": {
   ...:     "level2": {
   ...:       "level3": {
   ...:         "number": 3,
   ...:         "string": "more values"
   ...:       }
   ...:     }
   ...:   },
   ...:   "nested_invalid": { "numeric-id": 123, "from": "far away" },
   ...:   "optional_items": [1, 2, "3", "4", None, 5, 6, None]
   ...: }
   ...:

In [2]: from dict_typer import get_type_definitions

In [3]: print(get_type_definitions(source, show_imports=True))
from typing import List, Union

from typing_extensions import TypedDict


class NestedDict(TypedDict):
    number: int
    string: str


class Level2(TypedDict):
    level3: NestedDict


class MultipeLevels(TypedDict):
    level2: Level2


NestedInvalid = TypedDict("NestedInvalid", {
    "numeric-id": int,
    "from": str,
})


class Root(TypedDict):
    number_int: int
    number_float: float
    string: str
    list_single_type: List[str]
    list_mixed_type: List[Union[float, int, str]]
    nested_dict: NestedDict
    same_nested_dict: NestedDict
    multipe_levels: MultipeLevels
    nested_invalid: NestedInvalid
    optional_items: List[Union[None, int, str]]

Metadata

Release files for dict-typer 0.1.15

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dict-typer 0.1.15
File Size Uploaded
dict-typer-0.1.15.tar.gz 11.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for dict-typer 0.1.15
File Interpreter ABI Platform
dict_typer-0.1.15-py3-none-any.whl Python 3 none any Details

Total release size: 21.9 kB

Release files / dict-typer-0.1.15.tar.gz

Download URL dict-typer-0.1.15.tar.gz
Size 11.7 kB
Tags Source
SHA-256 checksum
How to use checksums
142bdd6695373839ea6f499876ff8270536c042aefced2abc3ea836b69764b38
BLAKE2b-256 checksum
How to use checksums
d27ec4508e1b5190b8b77915cbe0614f7fa72e1d82de0898379e96bb01e42a0c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/1.0.9 CPython/3.8.3 Darwin/19.5.0

Release files / dict_typer-0.1.15-py3-none-any.whl

Download URL dict_typer-0.1.15-py3-none-any.whl
Size 10.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
66baedee7235eff944e0a832513ad03429e0b3ca220c2fa0e438ca69996a311c
BLAKE2b-256 checksum
How to use checksums
81f7d7219ba6c2c71b05f1e44b96a6af91df862d3c8d46c6b4754fb2c6ffde80
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/1.0.9 CPython/3.8.3 Darwin/19.5.0

Release history Release notifications | RSS feed

This release

0.1.15 This release

2 release files

0.1.13

2 release files

0.1.12

2 release files

0.1.11

2 release files

0.1.10

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page