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

@hiscojs/json-updater

v1.1.0

Published

Type-safe, immutable JSON/JSONC updates with automatic formatting detection, comment preservation, and advanced array merging strategies

Readme

@hiscojs/json-updater

Type-safe, immutable JSON updates with automatic formatting detection and advanced array merging strategies.

Installation

npm install @hiscojs/json-updater

Quick Start

import { updateJson } from '@hiscojs/json-updater';

const jsonString = `{
  "server": {
    "host": "localhost",
    "port": 3000
  }
}`;

const { result } = updateJson({
  jsonString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.server,
      merge: () => ({ port: 8080 })
    });
  }
});

console.log(result);
// {
//   "server": {
//     "host": "localhost",
//     "port": 8080
//   }
// }

Features

  • Type-Safe: Full TypeScript support with generic type parameters
  • Immutable: Original JSON strings are never modified
  • JSONC Support: Parse and update JSON with Comments (tsconfig.json, settings.json, etc.)
  • Automatic Formatting Detection: Preserves indentation (spaces/tabs) and trailing newlines
  • Custom Formatting: Override with custom indentation if needed
  • Advanced Array Merging: Multiple strategies for merging arrays
  • Proxy-Based Path Tracking: Automatic path detection
  • Works with all JSON files: package.json, tsconfig.json, configuration files, etc.

API Reference

updateJson<T>(options)

Updates a JSON string immutably with type safety and formatting preservation.

Parameters

interface UpdateJsonOptions<T> {
  jsonString: string;
  annotate?: (annotator: {
    change: <L>(options: ChangeOptions<T, L>) => void;
  }) => void;
  formatOptions?: JsonFormatOptions;
}

interface JsonFormatOptions {
  indent?: number | string;        // Number of spaces or '\t' for tabs
  preserveIndentation?: boolean;   // Auto-detect from original (default: true)
  trailingNewline?: boolean;       // Add \n at end (default: auto-detect)
  allowComments?: boolean;         // Parse JSONC (JSON with Comments) (default: false)
}

Returns

interface JsonEdit<T> {
  result: string;           // Updated JSON string
  resultParsed: T;          // Parsed updated object
  originalParsed: T;        // Original parsed object
}

change<L>(options)

Defines a single change operation.

interface ChangeOptions<T, L> {
  findKey: (parsed: T) => L;
  merge: (originalValue: L) => Partial<L>;
}

Basic Usage

Simple Property Update

const jsonString = `{
  "name": "my-app",
  "version": "1.0.0"
}`;

const { result } = updateJson({
  jsonString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({ version: '1.0.1' })
    });
  }
});

// {
//   "name": "my-app",
//   "version": "1.0.1"
// }

Type-Safe Updates

interface PackageJson {
  name: string;
  version: string;
  dependencies: Record<string, string>;
  devDependencies?: Record<string, string>;
}

const { result, resultParsed } = updateJson<PackageJson>({
  jsonString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.dependencies,  // Fully typed!
      merge: (deps) => ({
        ...deps,
        'new-package': '^1.0.0'
      })
    });
  }
});

console.log(resultParsed.dependencies);  // Type-safe access!

Formatting Options

Automatic Detection (Default)

By default, formatting is automatically detected and preserved:

// 2-space indentation
const jsonString = `{
  "key": "value"
}`;

// 4-space indentation
const jsonString = `{
    "key": "value"
}`;

// Tab indentation
const jsonString = `{
\t"key": "value"
}`;

// All preserved automatically!
const { result } = updateJson({ jsonString, ... });

Custom Formatting

Override automatic detection with custom options:

const { result } = updateJson({
  jsonString,
  formatOptions: {
    indent: 4,                    // Use 4 spaces
    trailingNewline: true         // Add newline at end
  },
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({ key: 'value' })
    });
  }
});

Tab Indentation

const { result } = updateJson({
  jsonString,
  formatOptions: {
    indent: '\t'  // Use tabs
  },
  annotate: ({ change }) => { ... }
});

JSONC Support (JSON with Comments)

Enable JSONC support to work with configuration files that include comments like tsconfig.json, VSCode settings.json, etc.

Basic JSONC Usage

const jsoncString = `{
  // TypeScript configuration
  "compilerOptions": {
    "target": "ES2018", // Target version
    "module": "commonjs",
    "strict": true
  }
}`;

const { result } = updateJson({
  jsonString: jsoncString,
  formatOptions: {
    allowComments: true  // Enable JSONC support
  },
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.compilerOptions,
      merge: (original) => ({
        ...original,
        target: 'ES2020'
      })
    });
  }
});

// Comments are preserved in the output!

Update tsconfig.json

import fs from 'fs';
import { updateJson } from '@hiscojs/json-updater';

const tsconfigPath = 'tsconfig.json';
const tsconfig = fs.readFileSync(tsconfigPath, 'utf-8');

const { result } = updateJson({
  jsonString: tsconfig,
  formatOptions: {
    allowComments: true  // tsconfig.json uses JSONC format
  },
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.compilerOptions,
      merge: (opts) => ({
        ...opts,
        target: 'ES2022',
        lib: ['ES2022']
      })
    });
  }
});

fs.writeFileSync(tsconfigPath, result);

Update VSCode settings.json

const settingsJson = `{
  // Editor settings
  "editor.fontSize": 14,
  "editor.tabSize": 2,
  /* Theme configuration
     using dark theme */
  "workbench.colorTheme": "Dark+"
}`;

const { result } = updateJson({
  jsonString: settingsJson,
  formatOptions: {
    allowComments: true
  },
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        'editor.fontSize': 16,
        'editor.formatOnSave': true
      })
    });
  }
});

// Both single-line (//) and multi-line (/* */) comments are preserved

JSONC Comment Preservation

All comment styles are automatically preserved when editing:

const jsonString = `{
  // Server configuration
  "server": {
    "host": "localhost", // Current host
    "port": 3000
  }
}`;

const { result } = updateJson({
  jsonString,
  formatOptions: { allowComments: true },
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.server,
      merge: () => ({ host: 'production.example.com' })
    });
  }
});

// Output:
// {
//   // Server configuration
//   "server": {
//     "host": "production.example.com", // Current host
//     "port": 3000
//   }
// }

Supported comment styles:

  • Single-line: // comment
  • Multi-line: /* comment */
  • Inline: "key": "value" // comment
  • Mixed indentation (spaces/tabs)
  • Blank lines and spacing preserved

Note: Comments inside string values (like URLs with //) are never stripped

Array Merging Strategies

mergeByContents - Deduplicate by Deep Equality

import { updateJson, addInstructions } from '@hiscojs/json-updater';

const jsonString = `{
  "items": ["a", "b", "c"]
}`;

const { result } = updateJson({
  jsonString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'items',
          mergeByContents: true
        }),
        items: ['b', 'c', 'd']  // 'b' and 'c' deduplicated
      })
    });
  }
});

// { "items": ["a", "b", "c", "d"] }

mergeByName - Merge by Name Property

const jsonString = `{
  "containers": [
    { "name": "app", "image": "app:1.0" },
    { "name": "sidecar", "image": "sidecar:1.0" }
  ]
}`;

const { result } = updateJson({
  jsonString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'containers',
          mergeByName: true
        }),
        containers: [
          { name: 'app', image: 'app:2.0' }  // Updates 'app'
        ]
      })
    });
  }
});

// {
//   "containers": [
//     { "name": "app", "image": "app:2.0" },      // Updated
//     { "name": "sidecar", "image": "sidecar:1.0" } // Preserved
//   ]
// }

mergeByProp - Merge by Custom Property

const jsonString = `{
  "users": [
    { "id": 1, "name": "Alice" },
    { "id": 2, "name": "Bob" }
  ]
}`;

const { result } = updateJson({
  jsonString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'users',
          mergeByProp: 'id'
        }),
        users: [
          { id: 1, name: 'Alice Smith' },  // Updates id=1
          { id: 3, name: 'Charlie' }       // Adds new
        ]
      })
    });
  }
});

// {
//   "users": [
//     { "id": 1, "name": "Alice Smith" },  // Updated
//     { "id": 2, "name": "Bob" },          // Preserved
//     { "id": 3, "name": "Charlie" }       // Added
//   ]
// }

deepMerge - Deep Merge Nested Objects

const jsonString = `{
  "configs": [
    {
      "name": "database",
      "settings": {
        "timeout": 30,
        "pool": 10,
        "ssl": true
      }
    }
  ]
}`;

const { result } = updateJson({
  jsonString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'configs',
          mergeByName: true,
          deepMerge: true
        }),
        configs: [
          {
            name: 'database',
            settings: { timeout: 60 }  // Only update timeout
          }
        ]
      })
    });
  }
});

// {
//   "configs": [
//     {
//       "name": "database",
//       "settings": {
//         "timeout": 60,   // Updated
//         "pool": 10,      // Preserved
//         "ssl": true      // Preserved
//       }
//     }
//   ]
// }

Using originalValue

Access original values to make conditional updates:

const jsonString = `{
  "version": "1.2.3",
  "buildNumber": 42
}`;

const { result } = updateJson({
  jsonString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: (originalValue) => {
        const [major, minor, patch] = originalValue.version.split('.').map(Number);
        return {
          version: `${major}.${minor}.${patch + 1}`,
          buildNumber: originalValue.buildNumber + 1
        };
      }
    });
  }
});

// { "version": "1.2.4", "buildNumber": 43 }

Real-World Examples

package.json Updates

const packageJson = `{
  "name": "my-service",
  "version": "1.0.0",
  "dependencies": {
    "express": "^4.18.0",
    "lodash": "^4.17.21"
  }
}`;

const { result } = updateJson({
  jsonString: packageJson,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.dependencies,
      merge: (deps) => ({
        ...deps,
        express: '^4.19.0',  // Patch update
        axios: '^1.6.0'      // Add new
      })
    });
  }
});

Configuration Files

const configJson = `{
  "server": {
    "port": 3000,
    "host": "localhost"
  },
  "database": {
    "host": "localhost",
    "port": 5432
  }
}`;

const { result } = updateJson({
  jsonString: configJson,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.server,
      merge: () => ({ port: 8080 })
    });

    change({
      findKey: (parsed) => parsed.database,
      merge: () => ({ host: 'db.production.com' })
    });
  }
});

Best Practices

1. Use Type Parameters

// ✅ Good - Type safe
const { result } = updateJson<PackageJson>({ ... });

// ❌ Avoid - No type safety
const { result } = updateJson({ ... });

2. Leverage originalValue

// ✅ Good - Conditional based on original
merge: (originalValue) => ({
  version: bumpVersion(originalValue.version)
})

// ❌ Avoid - Hardcoded
merge: () => ({ version: '2.0.0' })

3. Use Merge Strategies for Arrays

// ✅ Good - Explicit merge strategy
...addInstructions({
  prop: 'dependencies',
  mergeByProp: 'name'
})

// ❌ Avoid - Array replacement
dependencies: newDeps  // Loses existing items

Dependencies

License

MIT

Repository

https://github.com/hisco/json-updater

Contributing

Issues and pull requests welcome!