npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

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

Readme

npm version CI bundle size license Ko-fi donate NPM Trusted Publishing

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-file
npm install zod-file
yarn add zod-file

YAML Support (Optional)

To use YAML files, install js-yaml:

pnpm add js-yaml

TOML Support (Optional)

To use TOML files, install smol-toml:

pnpm add smol-toml

Quick 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 file
  • save(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

  1. Sequential versioning – Migrations must form a sequential chain starting from version 1
  2. Chain completeness – The last migration must be for version currentVersion - 1
  3. Version field – Files include a _version field 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