Metadata-Version: 2.4
Name: wireform
Version: 1.0.0
Summary: Drop-in JSON encoder for dataclasses, datetimes, Decimal, bytes, sets, exceptions, and custom repr
Project-URL: Homepage, https://github.com/miriada-io/wireform
Project-URL: Repository, https://github.com/miriada-io/wireform
Project-URL: Issues, https://github.com/miriada-io/wireform/issues
Project-URL: Changelog, https://github.com/miriada-io/wireform/blob/master/CHANGELOG.md
Author-email: Miriada <info@miriada.io>
License-Expression: MIT
License-File: LICENSE
Keywords: dataclass,datetime,decimal,dumps,encoder,json,serialization
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
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
Requires-Python: >=3.11
Requires-Dist: typeful>=1.0.0
Provides-Extra: dev
Requires-Dist: coverage>=7.0; extra == 'dev'
Requires-Dist: pre-commit>=3.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# wireform

[![PyPI](https://img.shields.io/pypi/v/wireform.svg?label=PyPI)](https://pypi.org/project/wireform/)
[![Python](https://img.shields.io/pypi/pyversions/wireform.svg?label=Python)](https://pypi.org/project/wireform/)
[![Tests](https://github.com/miriada-io/wireform/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/miriada-io/wireform/actions/workflows/tests.yml)
[![License](https://img.shields.io/pypi/l/wireform.svg?label=License)](https://github.com/miriada-io/wireform/blob/master/LICENSE)

A `json.dumps` drop-in that serializes dataclasses, `datetime`, `Decimal`, `bytes`, sets, exceptions, and anything that implements `__repr_in_dumps__`.

## Installation

```bash
pip install wireform
```

Requires Python 3.11+.

## Why wireform?

The stdlib `json.dumps` refuses most real-world Python values:

- `json.dumps(MyDataclass(...))` → `TypeError` — dataclasses are not serializable out of the box.
- `json.dumps(datetime.now())` → `TypeError`; same for `date`, `time`, `Decimal`, `bytes`, `set`, `frozenset`, exceptions, and class objects.
- Silently accepting naive `datetime` is a trap — the reader can't tell if the timestamp is UTC or local. `wireform` refuses naive datetimes by design.
- `dataclasses.asdict` exists, but doesn't respect `field(repr=False)` and can't coexist with custom encoders for other types.

`custom_dumps` is a single callable that handles all of these without schemas, models, or configuration.

## Quick Start

```python
import datetime
from dataclasses import dataclass
from decimal import Decimal

from wireform import custom_dumps

@dataclass
class Payment:
    amount: Decimal
    currency: str
    paid_at: datetime.datetime

payment = Payment(
    amount=Decimal("19.99"),
    currency="EUR",
    paid_at=datetime.datetime(2026, 4, 18, 12, 0, tzinfo=datetime.UTC),
)

custom_dumps(payment)
# '{"amount": "19.99", "currency": "EUR", "paid_at": "2026-04-18T12:00:00+00:00"}'

custom_dumps({"tags": {"admin", "editor"}, "blob": b"hello"})
# '{"tags": ["admin", "editor"], "blob": "data:application/octet-stream;base64,aGVsbG8="}'

# Naive datetime — refused by design:
custom_dumps(datetime.datetime.now())
# TypeError: datetime.datetime WITHOUT tzinfo is not JSON serializable
```

## Overview

**Serialization:** [`custom_dumps`](#custom_dumps) | [`EnhancedJSONEncoder`](#enhancedjsonencoder) | [`ReprInDumps`](#reprindumps)

**File I/O:** [`read_json_file_by_path`](#read_json_file_by_path)

**Types:** [`JsonLoaded`](#type-aliases) | [`Jsonable`](#type-aliases) | [`CustomJsonable`](#type-aliases) | [`JsonDumper`](#jsondumper)

---

## Serialization

### `custom_dumps`

A `partial(json.dumps, cls=EnhancedJSONEncoder)`. Same signature as `json.dumps`, same return type (`str`), but accepts the extended set of Python values listed below.

```python
from wireform import custom_dumps

custom_dumps({"a": 1, "b": [1, 2, 3]})
# '{"a": 1, "b": [1, 2, 3]}'
```

Supported values beyond stdlib JSON:

| Python value             | JSON representation                                                       |
|--------------------------|---------------------------------------------------------------------------|
| `@dataclass` instance    | object of fields with `repr=True` (skips `ClassVar`, `InitVar`, `repr=False`) |
| `datetime` (tz-aware)    | ISO 8601 string — `"2026-04-18T12:00:00+00:00"`                           |
| `datetime` (naive)       | raises `TypeError`                                                        |
| `date`, `time`           | ISO 8601 string                                                           |
| `Decimal`                | string — `"19.99"`                                                        |
| `bytes`                  | data URL — `"data:application/octet-stream;base64,..."`                   |
| `set`, `frozenset`       | JSON array (unordered)                                                    |
| `type` (class object)    | `{"_t": "PY::class", "key": "<qualname>"}`                                |
| `Exception` instance     | `{"_t": "PY::Exception", "key": "<type qualname>", "args": [...]}`        |
| `ReprInDumps` subclass   | value returned by `__repr_in_dumps__()`                                   |

### `EnhancedJSONEncoder`

The underlying `json.JSONEncoder` subclass. Pass it to `json.dumps(..., cls=EnhancedJSONEncoder)` if you need the extra `dumps` kwargs directly rather than through `custom_dumps`.

```python
import json
from wireform import EnhancedJSONEncoder

json.dumps({"x": {1, 2, 3}}, cls=EnhancedJSONEncoder, indent=2)
```

### `ReprInDumps`

Mixin that lets a class control its own JSON form. Subclass it and either (a) override `__repr_in_dumps__` to return any `Jsonable` value, or (b) rely on the default, which delegates to `repr(self)` and produces a JSON string.

```python
from wireform import ReprInDumps, custom_dumps

class Tag(ReprInDumps):
    def __init__(self, name: str):
        self.name = name

    def __repr__(self):
        return f"Tag({self.name!r})"

custom_dumps(Tag("admin"))
# '"Tag(\'admin\')"'


class Version(ReprInDumps):
    def __init__(self, major: int, minor: int):
        self.major = major
        self.minor = minor

    def __repr_in_dumps__(self):
        return {"version": f"{self.major}.{self.minor}"}

custom_dumps(Version(1, 2))
# '{"version": "1.2"}'
```

## File I/O

### `read_json_file_by_path`

Open a file at the given path and parse it as JSON.

```python
from wireform import read_json_file_by_path

data = read_json_file_by_path("config.json")
```

Returns a `JsonLoaded` — the exact tree shape you get from `json.load`.

## Type Aliases

- `JsonLoaded` — the strict output shape of `json.load` / `json.loads`: `list`, `dict[str, ...]`, `str`, `int`, `float`, `None`.
- `Jsonable` — anything the **stdlib** `json.dumps` accepts: `JsonLoaded` plus `tuple`, and dicts with `int` keys.
- `CustomJsonable` — anything **`custom_dumps`** accepts: `Jsonable` plus `set`, `frozenset`, `bytes`, `ReprInDumps`, `type`, dataclasses, `datetime`, `date`, `time`, `Decimal`, `Exception`.

### `JsonDumper`

`typing.Protocol` matching the `json.dumps` signature. Use it when you want to type-hint a "something that behaves like `json.dumps`" parameter (e.g. to let callers swap `custom_dumps` for the stdlib version).

```python
from wireform import JsonDumper, custom_dumps

def render(data, *, dumper: JsonDumper = custom_dumps) -> str:
    return dumper(data, indent=2)
```

## A Note on Roundtripping

`custom_dumps` is a **one-way** encoder. Values like `set`, `bytes`, `Decimal`, class objects and exceptions are encoded to JSON-compatible forms, but `json.loads` will give you back the *encoded* shape (`list`, `str`, etc.), not the original Python type. Use `wireform` when you need to emit JSON — not when you need a serializer/deserializer round-trip.

## License

[MIT](https://github.com/miriada-io/wireform/blob/master/LICENSE)
