# Serde

> Rust-backed JSON and Avro serialization that returns Either instead of raising.

Source: https://docs.pyrula.io/primitives/serde/

`serialize` and `deserialize` return an [`Either`](https://docs.pyrula.io/primitives/either.md), so a bad payload is an `Err` rather than an exception.

## JSON

```python
from pyrula import JsonSerde

serde = JsonSerde()

serde.serialize({"id": 1, "name": "alice"})   # Ok(b'{"id":1,...}')
serde.deserialize(b'{"id": 1}')               # Ok({"id": 1})
serde.deserialize(b'not json')                # Err(DeserializationError(...))
```

Decoding matches `json.loads`: key order is kept and integers of any size are exact. It refuses `NaN`, `Infinity`, unpaired surrogate escapes and nesting deeper than about 200. The error names the line and column.

Encoding writes `None`, `bool`, `int`, finite `float`, `str`, `list` and `dict` with `str` keys. Anything else (a tuple, a non-string key, `nan`) gives `Err(SerializationError(...))`. Floats use shortest round-trip formatting, which can spell a value differently from `json.dumps` (`1e-7` against `1e-07`); compare by value. `JsonSchemaRegistrySerde`, the `par_map` JSON codec and Kafka's `produce_many_json` write the same way.

## Avro

`AvroSerde` takes a schema as a JSON string.

```python
from pyrula import AvroSerde

serde = AvroSerde(schema_json=SCHEMA)
serde.serialize({"id": 1, "name": "alice"})   # Ok(bytes)
```

`deserialize` takes exactly one datum. Trailing bytes are an `Err`, including a leftover Schema Registry frame (the error points at `AvroRegistrySerde`). Datums that end early, and values nested more than 500 deep, are refused.

Records and maps are `dict`, arrays `list`, `string` and `enum` `str`, `bytes` and `fixed` `bytes`, and a union holds whichever branch the value is. Named types may repeat or recurse.

Logical types match fastavro, and so confluent-kafka's Avro serializers:

| Logical type | Decodes to | Encodes from |
|---|---|---|
| `timestamp-millis`, `timestamp-micros` | UTC `datetime` | `datetime` (naive is local time) or `int` |
| `local-timestamp-millis`, `local-timestamp-micros` | naive `datetime` | `datetime` or `int` |
| `date` | `date` | `date`, ISO `str` or `int` |
| `time-millis`, `time-micros` | `time` | `time` or `int` |
| `decimal` (`bytes` or `fixed`) | `Decimal` | `Decimal` within precision and scale, or `bytes` |
| `uuid` | `UUID` | `UUID` or `str` |

Nanosecond timestamps and `duration` stay raw (`int`, 12 `bytes`). `big-decimal` gives an `Err`. A `uuid` on a `fixed` is refused when the serde is built.

Two options apply to `AvroSerde` and `AvroRegistrySerde`:

```python
AvroSerde(SCHEMA, strict=True)
AvroSerde(SCHEMA, logical_types="raw")
```

`strict=True` refuses fastavro's guessed conversions: a naive `datetime` for a `timestamp-*` field, a `datetime` for a `date`, and a decimal with more digits than its precision. `logical_types="raw"` decodes every logical type to its wire value.

## Batches

The `_many` methods take an [`IList`](https://docs.pyrula.io/primitives/collections.md) and run in one Rust call. `serialize_many` returns an `IList` of results. The packed variants return a [`BytesList`](https://docs.pyrula.io/primitives/numeric-lists.md): `serialize_many_raw` on `JsonSerde`, `serialize_many_bytes` on the Avro serdes.

```python
rows = IList([{"id": 1}, {"id": 2}])
json_serde.serialize_many(rows)               # IList([Ok(b'{"id":1}'), Ok(b'{"id":2}')])
packed = json_serde.serialize_many_raw(rows)  # BytesList
json_serde.deserialize_many_raw(packed)       # IList([{'id': 1}, {'id': 2}])
```

## Errors

```python
from pyrula import SerdeError, SerializationError, DeserializationError
```

Both specific errors subclass `SerdeError`.

## Schema Registry

`AvroRegistrySerde`, `JsonSchemaRegistrySerde` and `ProtobufRegistrySerde` handle Confluent framing, schema-id lookup and TLS/mTLS. They live on the Kafka path; see [Kafka](https://docs.pyrula.io/kafka.md).

```python
serde = AvroRegistrySerde.for_topic("http://registry:8081", "orders", SCHEMA)
```

`JsonSchemaRegistrySerde` frames and registers; it does not validate payloads against the schema.

Each takes `timeout_ms` (default 5000) for one registry request and caches a failed lookup for 5 seconds. A registry error names the registry (scheme, host and port, never a username or password from the URL), and a 401 or 403 adds a pointer to the registry credentials. [Formats](https://docs.pyrula.io/kafka/formats.md#when-the-registry-is-down) covers behavior when the registry is down.

With a pinned `reader_schema` that differs from the writer's, `AvroRegistrySerde` resolves through apache-avro, which does not keep map key order.
