Metadata-Version: 2.1
Name: json-data-types
Version: 0.2.0
Summary: Type hinting and validation for JSON-serializable Python data types.
Keywords: json,type-hints,typing,validation,jsondata,serialization
Author-Email: Sam McKelvie <dev@emckelvie.org>
License: MIT License
         
         Copyright (c) 2026 Samuel J. McKelvie
         
         Permission is hereby granted, free of charge, to any person obtaining a copy
         of this software and associated documentation files (the "Software"), to deal
         in the Software without restriction, including without limitation the rights
         to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
         copies of the Software, and to permit persons to whom the Software is
         furnished to do so, subject to the following conditions:
         
         The above copyright notice and this permission notice shall be included in all
         copies or substantial portions of the Software.
         
         THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
         IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
         FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
         AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
         LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
         OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
         SOFTWARE.
         
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Project-URL: Homepage, https://github.com/mckelvie-org/json-data-types
Project-URL: Source, https://github.com/mckelvie-org/json-data-types/tree/v0.2.0
Project-URL: Bug Tracker, https://github.com/mckelvie-org/json-data-types/issues
Project-URL: Changelog, https://github.com/mckelvie-org/json-data-types/releases
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# json-data-types

[![CI](https://img.shields.io/badge/CI-passing-brightgreen.svg)](https://github.com/mckelvie-org/json-data-types/actions/workflows/ci.yml)
[![PyPI version](https://img.shields.io/badge/pypi-v0.2.0-blue.svg)](https://pypi.org/project/json-data-types/0.2.0/)
[![Python versions](https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12%20|%203.13%20|%203.14-blue.svg)](https://pypi.org/project/json-data-types/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

`json-data-types` provides type aliases, validators, and a flexible object-oriented validation framework for Python values that are directly serializable to JSON — scalars, dicts, and lists, in both strict (`dict`/`list`) and duck-typed (`Mapping`/`Sequence`) flavors.

## Highlights

- **`JsonData`, `JsonDict`, `JsonList`** — strict type aliases requiring real `dict` and `list` objects.
- **`JsonDataLike`, `JsonDictLike`, `JsonListLike`** — duck-typed aliases accepting any `Mapping` or `Sequence`.
- **`JsonScalar`** — the union of the five JSON primitive types (`int`, `float`, `str`, `bool`, `None`).
- **`JsonScalarTypes`** — a runtime `tuple[type, ...]` constant suitable for `isinstance()` checks.
- **Validator functions** — `validate_json_*` and `loads_json_*` for each type alias, with deep or shallow validation and cyclic-reference detection.
- **`JsonObjectValidator`** — an extensible base class for building custom validators via subclassing.
- **`NotJsonDataError`** — a `ValueError` subclass raised on validation failure.
- **Zero dependencies** — pure Python, no third-party packages required.
- **Fully typed** — PEP 561 compliant, inline annotations, works with mypy strict mode.

## Installation

```bash
pip install json-data-types
```

## Type Hierarchy

```
JsonScalar       int | float | str | bool | None

JsonDataLike     JsonScalar | JsonListLike | JsonDictLike
  JsonListLike   Sequence[JsonDataLike]          # any Sequence (not str/bytes)
  JsonDictLike   Mapping[str, JsonDataLike]      # any Mapping with str keys

JsonData         JsonScalar | JsonList | JsonDict
  JsonList       list[JsonData]                  # real list only
  JsonDict       dict[str, JsonData]             # real dict only
```

Use the `*Like` variants when accepting data that may come from custom `Mapping` or `Sequence` implementations (e.g., `collections.OrderedDict`, `types.MappingProxyType`, `tuple`). Use the strict variants (`JsonData`, `JsonDict`, `JsonList`) when you need plain Python `dict` and `list` objects — the most common case when working with `json.loads()` output.

## Quick Start

```python
from json_data_types import JsonData, JsonDict, validate_json_data

# Annotate function parameters and return types
def process(payload: JsonData) -> JsonDict:
    assert isinstance(payload, dict)
    return {"status": "ok", "echo": payload}

# Validate and typecast untrusted data at a boundary
raw: object = {"key": [1, True, None, "hello"]}
data: JsonData = validate_json_data(raw)   # raises NotJsonDataError if invalid
```

## Type Aliases

### `JsonScalar`

```python
JsonScalar: TypeAlias = int | float | str | bool | None
```

Any JSON primitive value. Maps to JSON's `number` (integers and floats), `string`, `boolean`, and `null`.

### `JsonScalarTypes`

```python
JsonScalarTypes: Final[tuple[type, ...]] = (int, float, str, bool, NoneType)
```

A runtime constant holding the same types as `JsonScalar`, usable with `isinstance()`:

```python
from json_data_types import JsonScalarTypes

isinstance(42, JsonScalarTypes)    # True
isinstance([1, 2], JsonScalarTypes)  # False
```

### `JsonData`

```python
JsonData: TypeAlias = JsonScalar | JsonList | JsonDict
```

Any fully JSON-serializable Python value built exclusively from `dict`, `list`, and the scalar types. This is the type you get back from `json.loads()`.

### `JsonDict`

```python
JsonDict: TypeAlias = dict[str, JsonData]
```

A `dict` mapping `str` keys to `JsonData` values.

### `JsonList`

```python
JsonList: TypeAlias = list[JsonData]
```

A `list` of `JsonData` values.

### `JsonDataLike`

```python
JsonDataLike: TypeAlias = JsonScalar | JsonListLike | JsonDictLike
```

Like `JsonData` but accepts any `Mapping` or `Sequence` rather than only `dict` and `list`. Useful at API boundaries where callers may pass `OrderedDict`, `MappingProxyType`, named tuples, etc.

### `JsonDictLike`

```python
JsonDictLike: TypeAlias = Mapping[str, JsonDataLike]
```

Any `Mapping` with `str` keys and `JsonDataLike` values.

### `JsonListLike`

```python
JsonListLike: TypeAlias = Sequence[JsonDataLike]
```

Any `Sequence` of `JsonDataLike` values. `str` is excluded — it is treated as a scalar (`JsonScalar`), not as a sequence of characters.

## NotJsonDataError

All validators raise `NotJsonDataError` on failure. It is a subclass of `ValueError`, so existing `except ValueError` clauses continue to work.

```python
from json_data_types import NotJsonDataError, validate_json_data

try:
    validate_json_data((1, 2, 3))
except NotJsonDataError as e:
    print(e)  # Invalid top-level JSON data: (1, 2, 3) ...
```

## Validator Functions

### `validate_json_data(data, fast=False) -> JsonData`

Validates that `data` is a pure-Python JSON-serializable value. Raises `NotJsonDataError` if any value is not of an allowed type, if any dict key is not a `str`, or if a cyclic reference is detected.

```python
from json_data_types import validate_json_data

data = validate_json_data({"users": [{"id": 1, "score": 9.5, "active": True}]})
# data is typed as JsonData
```

With `fast=True`, only the top-level type is checked — no recursion, no cycle detection. Useful when you trust the structure but need a quick typecast.

```python
data = validate_json_data(payload, fast=True)  # shallow check only
```

### `validate_json_dict(data, fast=False) -> JsonDict`

Validates that `data` is a `dict` with `str` keys and `JsonData` values.

```python
cfg: JsonDict = validate_json_dict(raw_config)
```

### `validate_json_list(data, fast=False) -> JsonList`

Validates that `data` is a `list` of `JsonData` values.

```python
items: JsonList = validate_json_list(raw_items)
```

### `validate_json_data_like(data, fast=False) -> JsonDataLike`

Like `validate_json_data` but accepts any `Mapping` or `Sequence` rather than only `dict` and `list`.

```python
from collections import OrderedDict
from json_data_types import validate_json_data_like

od = OrderedDict([("a", 1), ("b", [2, 3])])
result = validate_json_data_like(od)  # passes
```

### `validate_json_dict_like(data, fast=False) -> JsonDictLike`

Validates that `data` is a `Mapping` with `str` keys and `JsonDataLike` values. Accepts any `Mapping` subtype at all nesting levels.

```python
from types import MappingProxyType
from json_data_types import validate_json_dict_like

mp = MappingProxyType({"key": [1, 2, 3]})
result = validate_json_dict_like(mp)  # passes
```

### `validate_json_list_like(data, fast=False) -> JsonListLike`

Validates that `data` is a `Sequence` of `JsonDataLike` values. `str` is excluded. Accepts any `Sequence` subtype (including `tuple`) at all nesting levels.

```python
from json_data_types import validate_json_list_like

result = validate_json_list_like((1, True, None, {"a": 1}))  # passes
```

## `loads_json_*` Functions

Each validator has a corresponding `loads_json_*` function that combines `json.loads()` with validation, returning the result cast to the appropriate type:

| Function | Returns |
|---|---|
| `loads_json_data(s, *, fast=False, **kwargs)` | `JsonData` |
| `loads_json_dict(s, *, fast=False, **kwargs)` | `JsonDict` |
| `loads_json_list(s, *, fast=False, **kwargs)` | `JsonList` |
| `loads_json_data_like(s, *, fast=False, **kwargs)` | `JsonDataLike` |
| `loads_json_dict_like(s, *, fast=False, **kwargs)` | `JsonDictLike` |
| `loads_json_list_like(s, *, fast=False, **kwargs)` | `JsonListLike` |

`s` may be `str`, `bytes`, or `bytearray`. Extra keyword arguments are forwarded to `json.loads()`.

```python
from json_data_types import loads_json_dict

config: JsonDict = loads_json_dict(response.content)
```

## Object-Oriented Validator API

Each validator is an instance of a concrete subclass of `JsonObjectValidator`. The singleton instances used by the module-level functions are:

| Instance | Class |
|---|---|
| *(used by `validate_json_data`)* | `JsonDataValidator` |
| *(used by `validate_json_dict`)* | `JsonDictValidator` |
| *(used by `validate_json_list`)* | `JsonListValidator` |
| *(used by `validate_json_data_like`)* | `JsonDataLikeValidator` |
| *(used by `validate_json_dict_like`)* | `JsonDictLikeValidator` |
| *(used by `validate_json_list_like`)* | `JsonListLikeValidator` |

Validator instances are callable (`__call__`) and have a `loads()` method, both with the same signature as the module-level functions. You can create your own instances directly:

```python
from json_data_types import JsonDataValidator, NotJsonDataError

v = JsonDataValidator()
data = v(raw)          # same as validate_json_data(raw)
data = v.loads(text)   # same as loads_json_data(text)
```

### Subclassing `JsonObjectValidator`

`JsonObjectValidator[_T]` is a generic base class. Override class variables to customize which types are accepted at each level:

| Class variable | Default | Purpose |
|---|---|---|
| `_scalar_types` | `JsonScalarTypes` | Types treated as scalars |
| `_dict_types` | `(dict,)` | Types treated as JSON objects |
| `_list_types` | `(list,)` | Types treated as JSON arrays |
| `_dict_key_types` | `(str,)` | Allowed dict key types |
| `_toplevel_types` | `None` (same as allowed) | Restrict the top-level type |
| `_excluded_types` | `(bytes, bytearray)` | Always-excluded types |
| `_excluded_scalar_types` | `()` | Excluded from scalar check |
| `_excluded_dict_types` | `()` | Excluded from dict check |
| `_excluded_list_types` | `(str, bytes, bytearray)` | Excluded from list check |
| `_excluded_toplevel_types` | `()` | Excluded at top level only |

Example — a validator that only accepts `dict` at the top level but uses duck-typed Mapping/Sequence checks internally:

```python
from collections.abc import Mapping, Sequence
from json_data_types import JsonObjectValidator, JsonDictLike

class StrictTopDuckDeepValidator(JsonObjectValidator[JsonDictLike]):
    _toplevel_types = (dict,)      # top level must be a real dict
    _dict_types = (Mapping,)       # nested dicts may be any Mapping
    _list_types = (Sequence,)      # nested lists may be any Sequence

v = StrictTopDuckDeepValidator()
```

## Common Patterns

### Validating API input

```python
from json_data_types import JsonDict, loads_json_dict

def handle_request(body: bytes) -> JsonDict:
    return loads_json_dict(body)   # parses + validates in one call
```

### Accepting flexible input

```python
from json_data_types import JsonDataLike, validate_json_data_like
import json

def serialize(value: JsonDataLike) -> str:
    validate_json_data_like(value)   # raises NotJsonDataError if invalid
    return json.dumps(value)
```

### Detecting cyclic structures before serialization

```python
from json_data_types import validate_json_data

bad: dict = {}
bad["self"] = bad   # cyclic!

validate_json_data(bad)
# NotJsonDataError: Cyclic reference detected in JsonDataLike object
```

### Using `fast=True` for trusted data

```python
from json_data_types import JsonData, validate_json_data

# Already validated upstream; just need the typecast
data: JsonData = validate_json_data(trusted_payload, fast=True)
```

### Checking scalar types at runtime

```python
from json_data_types import JsonScalarTypes

def is_json_scalar(v: object) -> bool:
    return isinstance(v, JsonScalarTypes)
```

## Supported Python Versions

Python 3.10 through 3.14.

## License

MIT. See [LICENSE](LICENSE).

---

For development and release workflow documentation, see [CONTRIBUTING.md](CONTRIBUTING.md).
