Skip to content
Pyrula
Pre-release: the packages are not on PyPI yet, so pip install does not resolve. Build from source to try it today.

Serde

serialize and deserialize return an Either, so a bad payload is an Err rather than an exception.

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.

AvroSerde takes a schema as a JSON string.

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 typeDecodes toEncodes from
timestamp-millis, timestamp-microsUTC datetimedatetime (naive is local time) or int
local-timestamp-millis, local-timestamp-microsnaive datetimedatetime or int
datedatedate, ISO str or int
time-millis, time-microstimetime or int
decimal (bytes or fixed)DecimalDecimal within precision and scale, or bytes
uuidUUIDUUID 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:

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.

The _many methods take an IList and run in one Rust call. serialize_many returns an IList of results. The packed variants return a BytesList: serialize_many_raw on JsonSerde, serialize_many_bytes on the Avro serdes.

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}])
from pyrula import SerdeError, SerializationError, DeserializationError

Both specific errors subclass SerdeError.

AvroRegistrySerde, JsonSchemaRegistrySerde and ProtobufRegistrySerde handle Confluent framing, schema-id lookup and TLS/mTLS. They live on the Kafka path; see Kafka.

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 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.

View this page as Markdown