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
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 anyT. 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 nestedundefined, non-finite numbers, cycles, and class instances.Decision
JSON.stringify/JSON.parseas the default serializer.JsonSerializer<T>generic; explicitly supplying anySerializer<T>is a trusted caller assertion.CachedOptions<Fn>conditional: JSON-compatible return types keep optionalserializer, while statically incompatible or unprovable types requireserializer: Serializer<CachedValue<Fn>>.cached()declaration. Cache layers are runtime-controlled, and the non-generic global Redis serializer cannot prove compatibility for an individual return type.undefinedsentinel, 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 nestedundefined,unknown, andany.The check is deliberately conservative and cannot prove runtime data. It cannot detect non-finite numbers, cycles/shared identity, runtime getters or
toJSONbehavior, or data-only classes that are structurally plain. Opaque, generic, and deeply recursive types may require an explicit serializer.Acceptance criteria
Serializer<T>is supplied.JsonSerializer<T>remains generic and the native JSON runtime path remains unchanged.Dateserializer round trip.Implementation: #50