Skip to content

Require explicit serializers for statically non-JSON cached values #16

Description

@lan17

Priority

P0 — Critical — resolve before the next release presented as production-ready.

Problem

JsonSerializer<T> uses native JSON, which can reject or change values while its generic API can still be instantiated for any T. A cached function can therefore return one runtime shape from fallback/request-local/process-local paths and a different shape after a Redis round trip.

Representative hazards include Date, Map, Set, bigint, functions, symbols, Buffers, typed arrays, required nested undefined, non-finite numbers, cycles, and class instances.

Decision

  • Keep native JSON.stringify / JSON.parse as the default serializer.
  • Keep JsonSerializer<T> generic; explicitly supplying any Serializer<T> is a trusted caller assertion.
  • Add no runtime validation or extra serialize/deserialize pass.
  • Make CachedOptions<Fn> conditional: JSON-compatible return types keep optional serializer, while statically incompatible or unprovable types require serializer: Serializer<CachedValue<Fn>>.
  • Apply the guard to every cached() declaration. Cache layers are runtime-controlled, and the non-generic global Redis serializer cannot prove compatibility for an individual return type.
  • Preserve the existing top-level undefined sentinel, Redis wire format, and fail-open handling for actual serializer exceptions.

V8 serialization or a pickle-like default is out of scope for this issue. It can be considered later as a separate opt-in codec if a concrete use case justifies it.

Static guard scope

The guard should accept JSON primitives, arrays, plain object/interface shapes, optional object fields, empty objects, and top-level undefined/void.

It should require an explicit serializer for known incompatible shapes including Date, Map, Set, bigint, functions, symbols, Buffers, typed arrays, method-bearing classes, required nested undefined, unknown, and any.

The check is deliberately conservative and cannot prove runtime data. It cannot detect non-finite numbers, cycles/shared identity, runtime getters or toJSON behavior, or data-only classes that are structurally plain. Opaque, generic, and deeply recursive types may require an explicit serializer.

Acceptance criteria

  • JSON-compatible cached functions compile without a serializer.
  • Representative known-incompatible types fail to compile without a per-function serializer.
  • The same types compile when a matching Serializer<T> is supplied.
  • JsonSerializer<T> remains generic and the native JSON runtime path remains unchanged.
  • Package-consumer declaration tests enforce the positive and negative matrix for ESM and CJS consumers.
  • Runtime tests cover unchanged native JSON semantics and a custom Date serializer round trip.
  • Documentation explains the guard, the trusted escape hatch, limitations, and the lack of additional runtime CPU overhead.

Implementation: #50

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions