# Type Registry Threading — Design

## Problem

`_deserialize_value` and `_serialize_value` in `serialize.py` import `default_registry` directly, ignoring the `type_registry` attribute on `Cendry`/`AsyncCendry`. Users who create a custom `TypeRegistry` and pass it to the context get no benefit — serialization always uses the global default.

## Approach

Thread `registry` as an explicit parameter through every serialization function. Public functions accept `registry: TypeRegistry | None = None` (defaults to `default_registry`). Internal functions receive the resolved registry directly.

## Signature Changes in `serialize.py`

### Internal functions (required `registry` param)

```python
def _deserialize_value(
    value: Any, hint: Any, *, enum_by: str = "value", registry: TypeRegistry
) -> Any:

def _serialize_value(
    value: Any, hint: Any, *, by_alias: bool, enum_by: str = "value", registry: TypeRegistry
) -> Any:

def deserialize_map(map_class: type, data: dict[str, Any], *, registry: TypeRegistry) -> Any:

def _map_to_dict(instance: Map, *, by_alias: bool, registry: TypeRegistry) -> dict[str, Any]:
```

Each replaces `from .types import default_registry` / `default_registry.get_handler(...)` with `registry.get_handler(...)`.

### Public functions (optional `registry` param)

```python
def deserialize[T: Model](
    model_class: type[T],
    doc_id: str | None,
    data: dict[str, Any],
    *,
    registry: TypeRegistry | None = None,
) -> T:

def from_dict[T: Model](
    model_class: type[T],
    data: dict[str, Any],
    *,
    doc_id: str | None = None,
    by_alias: bool = False,
    registry: TypeRegistry | None = None,
) -> T:

def to_dict(
    instance: Model | Map,
    *,
    by_alias: bool = False,
    include_id: bool = False,
    registry: TypeRegistry | None = None,
) -> dict[str, Any]:
```

Each resolves at the top: `registry = registry or default_registry`, then passes through all internal calls.

## `Query` / `AsyncQuery` Changes

`Query` and `AsyncQuery` call `deserialize` when iterating results and construct new instances internally. The registry must flow through all of these.

- Add `registry: TypeRegistry` as a keyword-only parameter to `Query.__init__` and `AsyncQuery.__init__` (stored as `self._registry`)
- All 4 `deserialize(...)` call sites become `deserialize(..., registry=self._registry)`
- All **10 internal construction sites** must propagate `self._registry`:
  - `Query`: `filter()`, `order_by()`, `limit()`, `first()`, `one()`
  - `AsyncQuery`: `filter()`, `order_by()`, `limit()`, `first()`, `one()`
- Context construction passes the registry: `Query(..., registry=self.type_registry)`

## Context Changes

`Cendry` and `AsyncCendry` pass `self.type_registry` to every call:

- **Reads:** `deserialize(..., registry=self.type_registry)` in `get`, `find`, `get_many`
- **Writes:** `to_dict(..., registry=self.type_registry)` in `save`, `create`
- **Queries:** `Query(q, model_class, self._apply_filter, self.type_registry)` in `select`, `select_group`

## Testing

Existing tests use `default_registry` and pass unchanged. New tests verify custom registry threading:

| Test case | What it verifies |
|-----------|-----------------|
| `to_dict` with custom registry | Custom handler's `serialize` is called |
| `from_dict` with custom registry | Custom handler's `deserialize` is called |
| `Cendry.get` with custom registry | Registry flows context → deserialize |
| `Cendry.save` with custom registry | Registry flows context → to_dict |
| `Query` iteration with custom registry | Registry flows context → query → deserialize |
| `Query.first()` / `Query.one()` with custom registry | Registry survives internal Query construction |
| `Query.filter().limit()` chain with custom registry | Registry survives chainable methods |
| Async variants | Mirror sync tests |

## Files Changed

- `src/cendry/serialize.py` — add `registry` parameter to all functions
- `src/cendry/query.py` — add `registry` to `Query`/`AsyncQuery` constructors, pass to `deserialize`
- `src/cendry/context.py` — pass `self.type_registry` to all serialize/deserialize/query calls
- `tests/test_serialize.py` — new tests for `from_dict`/`to_dict` with registry param
- `tests/test_context.py` — new tests for custom registry threading through context and queries

## Known Pre-existing Gap (out of scope)

`_cursor_value` in `_BaseCendry` uses `dataclasses.asdict` to convert Model instances for pagination cursors, bypassing `to_dict` and the type handler pipeline entirely. This means custom-type fields in cursor models won't be serialized correctly. This is a pre-existing issue unrelated to this change — fixing it is a separate task.

## No Breaking Changes

All new parameters default to `None`/`default_registry` in public functions. Existing user code works unchanged.
