# Encode

schema.encode(value) writes a valid value as the portable tree that decode reads. It is the inverse of decode:

from dataclasses import dataclass
from datetime import date
from pytypehint import struct_of

@dataclass
class Booking:
    day: date
    guests: int | float = 2

schema = struct_of(Booking)
booking = Booking(day=date(2026, 8, 8))
written = schema.encode(booking)

assert written == {"day": "2026-08-08", "guests": {"$type": "int", "$value": 2}}
assert schema.build(schema.decode(written)) == booking

Struct.encode takes an exact instance of its dataclass. Signature.encode takes a dictionary of arguments, such as the one build returns, and writes the keys it is given; it fills no defaults and rejects unknown keys.

The result is a new tree of plain dictionaries, lists, strings, numbers, booleans and None. The caller serializes it, for example with json.dumps. The value is never modified, and the tree shares no container with it.

Shape Written as
Date "YYYY-MM-DD"
Time "HH:MM:SS"
EnumShape Member name
List, Tuple Array
Struct Object of its fields
Others The value itself

# Unions

An option is named exactly where its portable spelling collides with another option of the same slot, following the portable contract: dataclass variants carry an inline $type, other colliding options use the $type/$value wrapper, and unambiguous options are written bare. These are the spellings to_dict() uses for defaults.

Options sharing a Python type, such as Annotated[list[str], Min(1)] | list[int], are told apart by the first option that accepts the value.

# Round trip

For a value that passes validation:

schema.build(schema.decode(schema.encode(value))) == value

The same holds after json.loads(json.dumps(...)). Types are preserved: floats stay floats, tuples stay tuples and each union value returns to its own option.

# Validation

Encode writes; it does not validate. It checks only what it needs in order to write: the root type, that each value belongs to an option of its slot, and fixed tuple lengths. Those failures raise SchemaTypeError or SchemaValueError with the field names and indexes of the value in path.

A value outside its limits, such as a number below Min, is written faithfully, and build rejects it when the tree is read back. Time fractions and offsets are kept in the text for the same reason. Non-finite floats are written as they are and have no standard JSON spelling. Cycles and excessive nesting can raise RecursionError.