docs/src/content/docs/programming_guide/serialization.mdx
CocoIndex serializes and caches the return values of memoized functions so that unchanged work can be skipped on subsequent runs. Most Python types work automatically — the key thing to get right is the return type annotation, which tells CocoIndex how to reconstruct your objects:
@coco.fn(memo=True)
async def process_chunk(chunk: Chunk) -> Embedding: # return type annotation
return embed(chunk.text)
Without annotations, values may deserialize as basic Python types (dict, list, str, etc.) instead of their original types.
:::info[Advanced: other places where serialization and type annotations matter] Serialization also applies to memo states and tracking records. If you're implementing these, add type annotations to:
__coco_memo_state__ prev_state parameter — annotate with the state type you return in MemoStateOutcome(state=...). See Memo state validation.reconcile() prev_possible_records parameter — annotate with Collection[YourTrackingRecord]. See Custom Target Connector.
:::The following types all work out of the box — no registration needed:
| Category | Types |
|---|---|
| Primitives | bool, int, float, str, bytes, None |
| Collections | list, tuple, dict, set, frozenset |
| Dataclasses | Any @dataclass (including frozen) |
| NamedTuples | Any NamedTuple subclass |
| Pydantic models | Any pydantic.BaseModel subclass |
| msgspec Structs | Any msgspec.Struct subclass |
| Date/time | datetime.datetime, datetime.date, datetime.time, datetime.timedelta, datetime.timezone |
| Other stdlib | uuid.UUID, complex, pathlib.Path, pathlib.PurePath |
| NumPy | numpy.ndarray, numpy.dtype (when numpy is installed) |
More generally, all types supported by msgspec work automatically. These types also work when nested inside collections or other structured types.
If your type isn't in the list above, register it with @coco.serialize_by_pickle:
import cocoindex as coco
@coco.serialize_by_pickle
class MySpecialType:
def __init__(self, data):
self.data = data
For third-party types, call it as a regular function:
import cocoindex as coco
from some_library import SomeType
coco.serialize_by_pickle(SomeType)
:::caution[Not for dataclasses, NamedTuples, or msgspec.Struct]
Don't apply @coco.serialize_by_pickle to dataclasses, NamedTuples, or msgspec.Struct — these are already supported natively. Applying it only works at the top level; when nested inside another supported type, the native encoding takes precedence and the decorator has no effect.
If serialization fails because of a problematic field inside a dataclass, register that field's type with @coco.serialize_by_pickle instead.
:::
Unions of a custom type with None work fine (MyDataclass | None). However, unions involving multiple custom types or a custom type with other non-None types require tagged msgspec.Struct variants.
For example, this won't work:
from dataclasses import dataclass
@dataclass
class Config:
value: int
class Settings(NamedTuple):
config: Config | str # fails at deserialization
Fix — wrap each variant in a tagged msgspec.Struct. The tag=True parameter embeds a type tag in the serialized data so that the correct variant can be identified during deserialization:
import msgspec
class ConfigValue(msgspec.Struct, tag=True):
value: int
class StringValue(msgspec.Struct, tag=True):
value: str
class Settings(NamedTuple):
config: ConfigValue | StringValue # works — variants are distinguished by tag
DeserializationError: Cannot build msgspec DecoderThis typically means an unsupported union type. The error message includes a hint about the cause.
Fix: Restructure the union to use tagged msgspec.Struct variants. See Union types above.
DeserializationError: Failed to deserialize msgspec payloadThe type annotation doesn't match the serialized data. Common causes:
-> YourType to the function signature.app.update(full_reprocess=True) or cocoindex update --full-reprocess.UnpicklingError: Forbidden global during unpickling_pickle.UnpicklingError: Forbidden global during unpickling: myapp.models.Summary
CocoIndex restricts which types can be deserialized for security. This error means your type isn't in the allow-list. Fix by either:
@coco.serialize_by_pickle to register the type:::note[Upgrading from older versions] If you see this error after upgrading, previously cached data may reference types that aren't yet registered. You have two options:
@coco.serialize_by_pickle to the type and re-run.@coco.unpickle_safe to allow reading the old cached data. Once the cache is rebuilt, the decorator can be removed.
:::