zod-file
v0.1.1
Published
Type-safe file persistence with Zod validation and schema migrations for Node.js. Supports JSON, YAML, and TOML.
Downloads
282
Maintainers
Readme
zod-file
A type-safe file persistence library with Zod validation and schema migrations for Node.js. Supports JSON out of the box, and YAML and TOML with optional dependencies.
Installation
pnpm add zod-filenpm install zod-fileyarn add zod-fileYAML Support (Optional)
To use YAML files, install js-yaml:
pnpm add js-yamlTOML Support (Optional)
To use TOML files, install smol-toml:
pnpm add smol-tomlQuick Start
JSON
import { z } from 'zod';
import { createZodJSON } from 'zod-file/json';
// Define your schema
const SettingsSchema = z.object({
theme: z.enum(['light', 'dark']),
fontSize: z.number().min(8).max(72),
});
// Create a persistence instance
const settings = createZodJSON({
schema: SettingsSchema,
default: { theme: 'light', fontSize: 14 },
});
// Load and save data
const data = await settings.load('./settings.json');
console.log(data.theme); // 'light' or 'dark'
await settings.save({ theme: 'dark', fontSize: 16 }, './settings.json');YAML
import { z } from 'zod';
import { createZodYAML } from 'zod-file/yaml';
const ConfigSchema = z.object({
database: z.object({
host: z.string(),
port: z.number(),
}),
features: z.array(z.string()),
});
const config = createZodYAML({
schema: ConfigSchema,
default: {
database: { host: 'localhost', port: 5432 },
features: [],
},
});
const data = await config.load('./config.yaml');
await config.save(data, './config.yaml');TOML
import { z } from 'zod';
import { createZodTOML } from 'zod-file/toml';
const ConfigSchema = z.object({
database: z.object({
host: z.string(),
port: z.number(),
}),
features: z.array(z.string()),
});
const config = createZodTOML({
schema: ConfigSchema,
default: {
database: { host: 'localhost', port: 5432 },
features: [],
},
});
const data = await config.load('./config.toml');
await config.save(data, './config.toml');API
createZodJSON(options)
Creates a persistence instance for typed JSON files.
createZodYAML(options)
Creates a persistence instance for typed YAML files. Requires js-yaml to be
installed.
createZodTOML(options)
Creates a persistence instance for typed TOML files. Requires smol-toml to be
installed.
createZodFile(options, serializer)
Creates a persistence instance with a custom serializer. Use this to add support for other file formats beyond JSON, YAML, and TOML. See Custom Serializers for details on creating your own serializer.
Options
| Property | Type | Required | Description |
| ------------ | ----------------- | -------- | ------------------------------------------------------------ |
| schema | z.ZodObject | Yes | The Zod schema for validating data |
| default | T \| () => T | No | Default value or factory when file is missing/invalid |
| version | number | No* | Current schema version (required if migrations are provided) |
| migrations | MigrationStep[] | No | Array of migration steps |
Serializer Interface
When using createZodFile, the second argument must implement the Serializer
interface. See Custom Serializers for details.
Returns
A ZodFile<T> object with:
load(path, options?)– Load and validate data from a filesave(data, path, options?)– Save data to a file
load(path, options?)
Loads data from a file, applies migrations if needed, and validates against the schema.
If a default is configured and loading fails for any reason (file missing,
invalid format, validation error, etc.), returns the default value instead of
throwing. Use throwOnError: true to throw errors even when a default is
configured.
Options
| Property | Type | Default | Description |
| -------------- | --------- | ------- | ---------------------------------------------- |
| throwOnError | boolean | false | Throw errors even when a default is configured |
save(data, path, options?)
Encodes data using the schema and writes it to a file.
Options
Options are format-specific. For JSON, the following option is available:
| Property | Type | Default | Description |
| --------- | --------- | ------- | ------------------------------------ |
| compact | boolean | false | Save without indentation (JSON only) |
YAML and TOML formats do not support save options. Custom serializers can define their own option types.
Versioned Schemas and Migrations
When your data schema evolves over time, use versioned schemas with migrations to handle backward compatibility.
import { z } from 'zod';
import { createZodJSON } from 'zod-file/json';
// Version 1 schema (historical)
const SettingsV1 = z.object({
theme: z.string(),
});
// Version 2 schema (current)
const SettingsV2 = z.object({
theme: z.enum(['light', 'dark']),
accentColor: z.string(),
});
const settings = createZodJSON({
version: 2 as const,
schema: SettingsV2,
migrations: [
{
version: 1,
schema: SettingsV1,
migrate: (v1) => ({
theme: v1.theme === 'dark' ? 'dark' : 'light',
accentColor: '#0066cc',
}),
},
],
});Migration Rules
- Sequential versioning – Migrations must form a sequential chain starting from version 1
- Chain completeness – The last migration must be for version
currentVersion - 1 - Version field – Files include a
_versionfield that is managed automatically
File Format
When using versions, files are saved with a _version field:
JSON:
{
"_version": 2,
"theme": "dark",
"accentColor": "#0066cc"
}YAML:
_version: 2
theme: dark
accentColor: '#0066cc'TOML:
_version = 2
theme = "dark"
accentColor = "#0066cc"When not using versions, the data is saved as-is without wrapping.
Error Handling
All errors are thrown as ZodFileError with a specific code for programmatic
handling:
import { ZodFileError } from 'zod-file';
try {
const data = await settings.load('./settings.json');
} catch (error) {
if (error instanceof ZodFileError) {
switch (error.code) {
case 'FileRead':
console.error('Could not read file:', error.message);
break;
case 'InvalidFormat':
console.error('File contains invalid JSON/YAML/TOML:', error.message);
break;
case 'InvalidVersion':
console.error('Missing or invalid _version field:', error.message);
break;
case 'UnsupportedVersion':
console.error('File version is newer than schema:', error.message);
break;
case 'Validation':
console.error('Data does not match schema:', error.message);
break;
case 'Migration':
console.error('Migration failed:', error.message);
break;
case 'MissingDependency':
console.error('Optional dependency not installed:', error.message);
break;
}
}
}Accessing the Underlying Error
The cause property contains the original error that triggered the failure.
This is useful for debugging or extracting detailed validation errors from Zod:
import { ZodFileError } from 'zod-file';
import { ZodError } from 'zod';
try {
const data = await settings.load('./settings.json');
} catch (error) {
if (error instanceof ZodFileError && error.code === 'Validation') {
if (error.cause instanceof ZodError) {
// Access Zod's detailed validation errors
for (const issue of error.cause.issues) {
console.error(`${issue.path.join('.')}: ${issue.message}`);
}
}
}
}Error Codes
| Code | Description |
| -------------------- | ----------------------------------------------------------------------- |
| FileRead | File could not be read from disk |
| FileWrite | File could not be written to disk |
| InvalidFormat | File content is not valid |
| InvalidVersion | _version field is missing, not an integer, or ≤ 0 |
| UnsupportedVersion | File version is greater than the current schema version |
| Validation | Data does not match the Zod schema |
| Migration | A migration function threw an error |
| Encoding | Schema encoding failed during save |
| MissingDependency | An optional dependency (like js-yaml or smol-toml) is not installed |
Advanced Usage
Custom Serializers
Use createZodFile with a custom serializer to support file formats beyond
JSON, YAML, and TOML. A serializer implements the Serializer interface with
encode, decode, and formatName properties.
Here's a simple CSV serializer for key-value pairs:
import { z } from 'zod';
import { createZodFile } from 'zod-file';
const csvSerializer = {
formatName: 'CSV',
decode(content: Buffer) {
const text = content.toString('utf-8');
const result: Record<string, string> = {};
for (const line of text.trim().split('\n')) {
const [key, value] = line.split(',');
result[key] = value;
}
return result;
},
encode(data: unknown): Buffer {
const text = Object.entries(data as Record<string, string>)
.map(([key, value]) => `${key},${value}`)
.join('\n');
return Buffer.from(text, 'utf-8');
},
};
const schema = z.object({
host: z.string(),
port: z.string(),
});
const config = createZodFile({ schema }, csvSerializer);
const data = await config.load('./config.csv');
await config.save({ host: 'localhost', port: '3000' }, './config.csv');Custom Serializer Options
Serializers can define custom options for decoding and encoding. The
Serializer type accepts two type parameters: load options and save options.
import { z } from 'zod';
import { createZodFile } from 'zod-file';
type XMLLoadOptions = {
/** Strip XML comments before decoding */
stripComments?: boolean;
};
type XMLSaveOptions = {
/** Omit the XML declaration */
omitDeclaration?: boolean;
/** Indentation string (default: 2 spaces) */
indent?: string;
};
const xmlSerializer = {
formatName: 'XML',
decode(content, options?: XMLLoadOptions) {
let xml = content.toString('utf-8');
if (options?.stripComments) {
xml = xml.replace(/<!--[\s\S]*?-->/g, '');
}
// ... decode XML to object
return decodeXML(xml);
},
encode(data, options?: XMLSaveOptions) {
const indent = options?.indent ?? ' ';
const declaration = options?.omitDeclaration
? ''
: '<?xml version="1.0"?>\n';
// ... convert object to XML
const xml = declaration + toXML(data, indent);
return Buffer.from(xml, 'utf-8');
},
};
const schema = z.object({
server: z.object({
host: z.string(),
port: z.number(),
}),
});
const config = createZodFile({ schema }, xmlSerializer);
// Load with custom options
const data = await config.load('./config.xml', { stripComments: true });
// Save with custom options
await config.save(data, './config.xml', {
omitDeclaration: true,
indent: '\t',
});Async Migrations
Migration functions can be async for complex transformations:
const settings = createZodJSON({
version: 2 as const,
schema: SettingsV2,
migrations: [
{
version: 1,
schema: SettingsV1,
migrate: async (v1) => {
// Perform async operations if needed
const defaultAccent = await fetchDefaultAccentColor();
return {
theme: v1.theme === 'dark' ? 'dark' : 'light',
accentColor: defaultAccent,
};
},
},
],
});Default Value Factory
Use a factory function for defaults that should be computed fresh each time:
const settings = createZodJSON({
schema: SettingsSchema,
default: () => ({
theme: 'light',
lastOpened: new Date().toISOString(),
}),
});License
Apache-2.0
