@gialicus/smart-object
v2.0.2
Published
Oggetti TypeScript generati da schemi Zod con getter, set* tipizzati e operazioni JSON Patch
Maintainers
Readme
@gialicus/smart-object
Typed TypeScript objects backed by Zod schemas, with an RFC 6902 operation log for every validated change. Use them when you need mutable, type-safe state plus a portable delta trail for audit, sync, or replay — without bolting on a separate change-tracking layer.
Installation
npm install @gialicus/smart-object zodDependencies: Zod ^4.0.0 (peer dependency — schema validation) and fast-json-patch (RFC 6902 patch application, bundled).
Usage
import z from "zod";
import { SmartObject } from "@gialicus/smart-object";
const Person = SmartObject(z.object({
name: z.string(),
age: z.number(),
}));
const person = new Person({ name: "Mario", age: 30 });
// Reads expose validated state
console.log(person.name); // "Mario"
// Writes validate first, then append patches to person.operations
person.setName("Luigi");
person.setAge(31);
console.log(person.operations);
// [
// { op: "replace", path: "/name", value: "Luigi" },
// { op: "replace", path: "/age", value: 31 },
// ]
person.setName("Luigi"); // Unchanged value — no operation added (keeps sync payloads minimal)
person.clearOperations(); // Drops the audit trail after persist/sync; state is unchanged
// Snapshot for serialization — deep clone, safe for JSON.stringify
console.log(person.toJSON());
// Initial construction is the replay baseline — it never emits operations
console.log(new Person({ name: "Mario", age: 30 }).operations); // []
// Reconstruct from baseline + accumulated deltas
const initial = { name: "Mario", age: 30 };
const person2 = Person.fromOperations(initial, [...person.operations]);Union root schemas
SmartObject also accepts z.discriminatedUnion(...) and z.union([...]) when every option is a z.object(...):
const Event = SmartObject(z.discriminatedUnion("type", [
z.object({ type: z.literal("click"), x: z.number(), y: z.number() }),
z.object({ type: z.literal("scroll"), delta: z.number() }),
]));
const event = new Event({ type: "click", x: 10, y: 20 });
event.setX(15);
// Switch the active union variant atomically
event.switchToScroll({ delta: 5 });
// or: event.switchVariant({ type: "scroll", delta: 5 });See examples/event.ts and examples/profile.ts for full demos.
Object and array getters always return deep clones — in-place mutation does not affect internal state or the operation log. Only set* methods generate RFC 6902 operations.
API
SmartObject(schema)
Factory that accepts a Zod schema and returns an instantiable class.
| Parameter | Type | Description |
|-----------|------|-------------|
| schema | z.ZodObject | z.ZodUnion | z.ZodDiscriminatedUnion | z.ZodIntersection | z.ZodLazy | Zod schema defining the object shape |
Members generated for each schema field foo:
| Member | Type | Description |
|--------|------|-------------|
| foo | getter | Exposes the current validated field value (deep clone for objects and arrays) |
| setFoo(value) | (value: T) => void | Validates, updates state, and records patches only when the value actually changes |
set* method names follow camelCase with the field name capitalized (name → setName, userId → setUserId).
Union root extras (discriminated and generic unions):
| Member | Type | Description |
|--------|------|-------------|
| switchVariant(value) | (variant) => void | Replaces the entire active variant after full schema validation |
| switchTo{Variant}(fields) | (fields) => void | Discriminated unions only — switches to a variant without repeating the discriminator (e.g. switchToScroll({ delta: 5 })) |
Record and Map field extras (for each z.record(...) field or string-key z.map(...) field tags):
| Member | Type | Description |
|--------|------|-------------|
| getTagsEntry(key) | (key: string) => V \| undefined | Reads a single entry |
| setTagsEntry(key, value) | (key: string, value: V) => void | Validates and patches a single entry (/tags/{key}) |
| deleteTagsEntry(key) | (key: string) => void | Removes an entry |
Instance members:
| Member | Type | Description |
|--------|------|-------------|
| operations | readonly Operation[] | Chronological RFC 6902 patch log (defensive copy) |
| clearOperations() | () => void | Clears the patch log without rolling back state |
| toJSON() | () => T | Deep clone of current state, safe for JSON.stringify |
Static members:
| Member | Type | Description |
|--------|------|-------------|
| fromOperations(initial, operations) | (initial, Operation[]) => Instance | Builds an instance from a baseline, replays and validates operations, and copies them into the accumulator |
SmartObjectError
Structured error thrown on validation failures, invalid union field access, and failed replay:
| Property | Type | Description |
|----------|------|-------------|
| code | "InvalidValue" | "InvalidUnionField" | "InvalidReplay" | "UnsupportedSchema" | Error category |
| field | string | undefined | Schema field path when applicable |
| cause | unknown | Original error (e.g. ZodError) |
Operation
RFC 6902 operation emitted by fast-json-patch when a field actually changes. Examples:
{ op: "replace", path: "/name", value: "Luigi" }
{ op: "add", path: "/age", value: 30 }Exported types
Operation— JSON Patch operation (re-export fromfast-json-patch)SmartObjectSchema— union of Zod schema types accepted bySmartObject()SmartObjectErrorCode—"InvalidValue"|"InvalidUnionField"|"InvalidReplay"|"UnsupportedSchema"SetMethods<T>— mapped type of inferredset*methods for shapeTSetMethodsUnion<T>—set*methods for union root schemasAllKeys<T>— all keys across union membersUnionDataShape<U>— flattened data shape for union rootsVariantSwitchMethods<T>—switchVariantfor union rootsDiscriminatedVariantSwitchMethods<T, D>—switchVariantplus generatedswitchTo*methodsRecordFieldMethods<T>— dynamic entry accessors forz.recordand string-keyz.mapfieldsOperationsAccessor—operationsandclearOperations()SnapshotAccessor<T>—toJSON()SmartObjectConstructor<T>— constructor type includingfromOperationsSmartObjectInstance<T>— full instance type (getters + set* + operations + toJSON)
Limitations
- Partial discriminator write — Changing a discriminated union discriminator alone via
setType(...)without providing the new variant fields throwsSmartObjectError. UseswitchVariant(...)orswitchTo{Variant}(...)instead. - Union field on wrong variant — Setting a field that does not exist on the active variant throws
SmartObjectError. - Date fields —
z.date()andz.coerce.date()are supported; operations store ISO 8601 strings while getters returnDateinstances. - Map, Set, and bigint —
z.map(string keys),z.set, andz.bigint()are supported with explicit codecs; operations use JSON-safe plain objects, arrays, and decimal strings respectively. String-keyz.mapfields also expose per-entryget/set/delete*Entrymethods; whole-fieldset*is used forz.setand for map fields with non-string keys (not supported). Non-string map keys are not supported. - Transforms —
z.transform/z.pipewith preprocessing work at runtime; operations store the output value after validation. TypeScript setter input types may not reflect transforms.
Design rationale
- Construction —
new Person(initial)validates and seeds internal state without emitting operations, because that snapshot is the baseline every later patch is measured against. - Validation — Each write is validated against the schema so the operation log only records structurally valid changes.
- No-op writes — Identical values are skipped to keep the patch log minimal and suitable for network sync.
- Patch-based updates — Changes are expressed as RFC 6902 operations so deltas are standard, composable, and replayable.
- Operation accumulation — Patches from
compareare appended in order, preserving causality for audit and replay. - Replay —
fromOperations(initial, operations)replays patches, re-validates with Zod, and requires the same baseline used when the operations were produced.
Examples
examples/person.ts— primitives, nested objects, and arraysexamples/event.ts— discriminated union rootexamples/profile.ts— generic union rootexamples/record.ts—z.recordand string-keyz.mapentry API
Project structure
smart-object/
├── src/
│ ├── index.ts # Public API barrel export
│ ├── types.ts # Operation and inferred types
│ ├── errors.ts # SmartObjectError
│ ├── zod-introspect.ts # Zod schema introspection
│ └── smart-object/
│ ├── index.ts # Re-export SmartObject
│ ├── factory.ts # Public SmartObject() factory
│ ├── build-class.ts # Class generation orchestration
│ ├── instance-state.ts # WeakMap-backed instance storage
│ ├── read-field.ts # Defensive getter reads
│ ├── json-patch.ts # fast-json-patch wrapper + Date-safe deepClone
│ ├── codecs.ts # JSON-safe codecs for Date, Map, Set, and bigint
│ ├── apply-operations.ts # Replay and rollback
│ ├── union-variant.ts # Union variant matching
│ ├── define-prototype.ts # Getter/setter prototype setup
│ └── setters/
│ ├── object-field.ts
│ ├── union-field.ts
│ ├── variant-switch.ts
│ └── record-field.ts
├── examples/
│ ├── person.ts
│ ├── event.ts
│ ├── profile.ts
│ └── record.ts
├── tests/
│ ├── fixtures/
│ │ ├── person.ts
│ │ ├── entity.ts
│ │ ├── event.ts
│ │ └── profile.ts
│ ├── smart-object/
│ │ ├── construction.test.ts
│ │ ├── getters.test.ts
│ │ ├── setters.test.ts
│ │ ├── clear-operations.test.ts
│ │ ├── from-operations.test.ts
│ │ ├── union-fields.test.ts
│ │ ├── discriminated-union-root.test.ts
│ │ ├── generic-union-root.test.ts
│ │ ├── robustness.test.ts
│ │ ├── setter-naming.test.ts
│ │ ├── to-json.test.ts
│ │ ├── schema-variants.test.ts
│ │ ├── record-fields.test.ts
│ │ ├── date-codec.test.ts
│ │ ├── complex-schema-types.test.ts
│ │ ├── intersection-lazy.test.ts
│ │ └── types.test.ts
│ └── zod-introspect.test.ts
└── dist/ # Build output (generated)