hat.json - Python JSON data library

Hat uses JSON data as simple data structures supported by various platforms with well supported human-readable encoding formats.

Under JSON data, following data types are assumed:

  • constants null, true and false

  • Numbers (integer and floating point)

  • String

  • Array

  • Object

Python data mappings

Mapping between JSON data types and built in types:

JSON data

Python data

null

None

true, false

bool

Number

int, float

String

str

Array

list

Object

dict

According to previous mapping, this library defines following types:

Array: typing.TypeAlias = typing.List['Data']
Object: typing.TypeAlias = typing.Dict[str, 'Data']
Data: typing.TypeAlias = None | bool | int | float | str | Array | Object

Because in Python bool is subtype of int, == operator can’t be used if strict comparison between JSON data is required. In this cases, function hat.json.equals can be used:

def equals(a: Data, b: Data) -> bool: ...

Example usage:

assert equals(0, 0.0) is True
assert equals({'a': 1, 'b': 2}, {'b': 2, 'a': 1}) is True
assert equals(1, True) is False

Function hat.json.clone enables deep cloning of composite JSON data:

def clone(data: Data) -> Data: ...

Example usage:

x = {'a': [1, 2, 3]}
y = clone(x)
assert x is not y
assert x['a'] is not y['a']
assert equals(x, y)

Additional utility function hat.json.flatten is provided. This generator is used for recursively flattening of Array’s:

def flatten(data: Data) -> typing.Iterable[Data]: ...

Example usage:

data = [1, [], [2], {'a': [3]}]
result = [1, 2, {'a': [3]}]
assert list(flatten(data)) == result

JSON data path

Path can be used as efficient reference to subset of deeply nested JSON data structure:

Path: typing.TypeAlias = int | str | typing.List['Path']

Determining data subset is defined by recursive algorithm which takes into account Path type:

  • int

    References specific element of input Array with index equal to path.

  • str

    References specific element of input Object with key equal to path.

  • list

    If path is empty list, input data as a whole is referenced. If list has at least one element and head, rest = path[0], path[1:], referenced data is equal of applying rest path to data obtained as result of applying head path onto input data.

Function hat.json.get is used for obtaining subset of input data referenced by path. If referenced subset doesn’t exist, this function returns default value:

def get(data: Data, path: Path, default: Data | None = None) -> Data: ...

Example usage:

data = {'a': [1, 2, [3, 4]]}
path = ['a', 2, 0]
assert get(data, path) == 3

data = [1, 2, 3]
assert get(data, 0) == 1
assert get(data, 5) is None
assert get(data, 5, default=123) == 123

Function hat.json.set_ is used for creating new data based on input data where subset of input data is replaced by provided input value. This function doesn’t modify input data and tries to optimally reuse parts of input data which are the same as in output data:

def set_(data: Data, path: Path, value: Data) -> Data: ...

Example usage:

data = [1, {'a': 2, 'b': 3}, 4]
path = [1, 'b']
result = set_(data, path, 5)
assert result == [1, {'a': 2, 'b': 5}, 4]
assert result is not data

data = [1, 2, 3]
result = set_(data, 4, 4)
assert result == [1, 2, 3, None, 4]

Function hat.json.remove is used for creating new data based on input data where subset of input data referenced by path is removed. This function doesn’t modify input data and tries to optimally reuse parts of input data which are the same as in output data:

def remove(data: Data, path: Path) -> Data: ...

Example usage:

data = [1, {'a': 2, 'b': 3}, 4]
path = [1, 'b']
result = remove(data, path)
assert result == [1, {'a': 2}, 4]
assert result is not data

data = [1, 2, 3]
result = remove(data, 4)
assert result == [1, 2, 3]

JSON patch

Function hat.json.diff and hat.json.patch provide simple wrappers for jsonpatch library (implementation of JSON Patch):

def diff(src: Data, dst: Data) -> Data: ...

def patch(data: Data, diff: Data) -> Data: ...

Example usage:

src = [1, {'a': 2}, 3]
dst = [1, {'a': 4}, 3]
result = diff(src, dst)
assert result == [{'op': 'replace', 'path': '/1/a', 'value': 4}]

data = [1, {'a': 2}, 3]
d = [{'op': 'replace', 'path': '/1/a', 'value': 4}]
result = patch(data, d)
assert result == [1, {'a': 4}, 3]

Encoding/decoding

Encoding of JSON data can be based on JSON, YAML or TOML format:

class Format(enum.Enum):
    JSON = 'json'
    YAML = 'yaml'
    TOML = 'toml'

Encoding/decoding implementations used in hat.json are based on json standard library , PyYAML library, tomli library and tomli-w library .

For encoding to string, functions hat.json.encode and hat.json.decode can be used:

def encode(data: Data,
           format: Format = Format.JSON,
           indent: int | None = None
           ) -> str:

def decode(data_str: str,
           format: Format = Format.JSON
           ) -> Data:

For encoding to file, functions hat.json.encode_file and hat.json.decode_file can be used. If format is not set, it will be derived from path suffix:

def encode_file(data: Data,
                path: pathlib.PurePath,
                format: Format | None = None,
                indent: int | None = 4):

def decode_file(path: pathlib.PurePath,
                format: Format | None = None
                ) -> Data:

If encoding to opened streams is required, functions hat.json.encode_stream and hat.json.decode_stream can be used:

def encode_stream(data: Data,
                  stream: io.TextIOBase | io.RawIOBase,
                  format: Format = Format.JSON,
                  indent: int | None = 4):

def decode_stream(stream: io.TextIOBase | io.RawIOBase,
                  format: Format = Format.JSON
                  ) -> Data:

JSON Schema

JSON Schema provides means for definition and validation of JSON data structures.

hat.json.SchemaRepository provides wrapper for jsonschema library with ability to utilize multiple interconnected JSON schemas.

All schemas combined in single SchemaRepository can be serialized as JSON data.

class SchemaRepository:

    def __init__(self, *args: typing.Union[pathlib.PurePath,
                                           Data,
                                           'SchemaRepository']): ...

    def validate(self,
                 schema_id: str,
                 data: Data): ...

    def to_json(self) -> Data: ...

    @staticmethod
    def from_json(data: pathlib.PurePath | Data
                  ) -> 'SchemaRepository': ...

API

API reference is available as part of generated documentation: