@krishanmarco/epizod
v0.0.3
Published
Isomorphic (browser + Node.js) TypeScript library, published as dual CJS + ESM.
Downloads
499
Readme
@krishanmarco/epizod
Serialize Zod schemas to JSON and back, and render them to TypeScript source. The same source runs unchanged in the browser and in Node.js, and ships as dual CJS + ESM.
What it does
epizod does two things with a Zod schema:
- Serialize and deserialize.
epizodSerializeturns a schema into a JSON-safe value you can store in atextorjsonbcolumn;epizodDeserializerebuilds an equivalent schema from it. The round-trip preserves both.parsebehaviour and the TypeScript render. - Render to TypeScript.
epizodZodToTypescriptturns a schema into its TypeScript type source — objects, unions, tuples, function signatures, the exotic types (date/bigint/map/set/promise/…), and recursive schemas (emitted as namedAuxiliary_Naliases).
The serialized form is branded (EpiSerializable), so a raw object — or another
library's serialized schema, such as a JSON Schema — cannot reach epizodDeserialize.
Produce one with epizodSerialize, or validate untrusted input (a database read) with
epizodAsSerializable.
Install
pnpm add @krishanmarco/epizod zodzod is a peer dependency, so epizod uses the schema library you already have. The only
runtime dependency is typescript, which the renderer calls at runtime.
Usage
Store a schema and read it back
import { epizodSerialize, epizodAsSerializable, epizodDeserialize } from '@krishanmarco/epizod';
import { z } from 'zod';
const contract = z.object({
name: z.string().describe('A human label'),
startedAt: z.date(),
retryIds: z.set(z.string()),
total: z.bigint(),
mode: z.enum(['fast', 'slow']),
});
// Serialize to a JSON-safe value and persist it.
const stored = epizodSerialize(contract); // EpiSerializable
await db.save(JSON.stringify(stored));
// Later: validate the untrusted read, then rebuild the schema.
const serialized = epizodAsSerializable(JSON.parse(await db.load()));
const schema = epizodDeserialize(serialized);
schema.parse({ name: 'x', startedAt: new Date(), retryIds: new Set(), total: 1n, mode: 'fast' });Render a schema to TypeScript
import { epizodZodToTypescript } from '@krishanmarco/epizod';
import { z } from 'zod';
epizodZodToTypescript(z.object({ a: z.string(), b: z.number().optional() }));
// {
// a: string;
// b?: number;
// }API
| Function | Signature | Purpose |
|----------|-----------|---------|
| epizodSerialize | (schema, options?) => EpiSerializable | Zod schema → JSON-safe branded value. Throws on a construct it cannot carry. |
| epizodDeserialize | (serialized, options?) => z.ZodType | EpiSerializable → equivalent Zod schema. |
| epizodAsSerializable | (value) => EpiSerializable | Validate an untrusted value as a serialized schema — the only sanctioned way to brand outside input. Throws otherwise. |
| epizodZodToTypescript | (schema, options?) => string | Zod schema → TypeScript type source. |
DeserializeOptions carries transforms — re-supplied implementations for any transform
captured in the schema, because a function body is never serialized. ZodToTypescriptOptions
carries optionalPropertyStyle ('question-mark' or 'undefined-union') and
singleLineComments.
Nomenclature
The vocabulary of the domain. Add a row the moment a term earns a name, before it drifts into three synonyms.
| Term | Meaning |
|------|---------|
| serialize | Turn a Zod schema into an EpiSerializable (epizodSerialize). |
| deserialize | Rebuild an equivalent Zod schema from an EpiSerializable (epizodDeserialize). |
| render | Turn a Zod schema into TypeScript type source (epizodZodToTypescript). |
| round-trip | serialize → deserialize; faithful in both .parse and render. |
| EpiSerializable | The branded, JSON-safe serialized form of a schema — the stored format. |
| auxiliary type | A generated type Auxiliary_N = … alias standing in for a recursive schema in the render. |
Package shape
- Isomorphic — the same source runs in the browser and in Node.js.
- Dual format —
tsupemits ESM (dist/index.mjs) and CJS (dist/index.js) from one source, with per-format type declarations (.d.mtsand.d.ts). zodandtypescriptstay external —zodresolves against your install (the peer),typescriptagainst epizod's own.
Development
| Script | What it does |
|--------|--------------|
| pnpm build | Bundle CJS + ESM + type declarations into dist/ (tsup) |
| pnpm typecheck | tsc --noEmit over the source |
| pnpm test | Run the mocha + chai suite (src/**/*.test.ts) via tsx |
| pnpm lint | ESLint (with dprint formatting) using the vendored SDK config |
| pnpm lint:fix | Same, applying autofixes |
src/index.test.ts pins the public surface by snapshot: the serialized JSON is a stored
format and the rendered TypeScript is generated code, so both are asserted whole and cannot
drift silently.
Repository layout
| Path | Purpose |
|------|---------|
| src/index.ts | The public barrel — the only published entry point. |
| src/serializer/ | epizodSerialize / epizodDeserialize / epizodAsSerializable and the EpiSerializable brand. |
| src/to-typescript/ | epizodZodToTypescript. |
| src/modules/zodex/ | Vendored schema ↔ JSON codec (zerialize / dezerialize), adapted to Zod 4. |
| src/modules/zod-to-ts/ | Vendored schema → TypeScript renderer, adapted to Zod 4. |
| sdk/ | Vendored slice of the Epilogo SDK's src/dev/ — the ESLint config, import-boundary plugin, and expect test helper — so @epilogo/sdk/dev/... imports resolve without depending on the SDK. |
| dev/ | Test infrastructure (the snapshot helper and its __snapshots__). Never bundled or published. |
| patches/ | pnpm patch disabling dprint's Dockerfile formatter. |
| tsup.config.ts | Dual CJS/ESM build configuration. |
Linting and formatting
ESLint is wired through .eslintrc.js, which loads the vendored config at
sdk/dev/eslint/eslintrc.js — the same configuration and custom import-boundary plugin the
SDK uses, copied in because epizod has no SDK dependency. Refresh it by re-copying from the
SDK's src/dev/eslint/.
