JSPM

@hiscojs/json-updater

1.1.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 4
  • Score
    100M100P100Q18607F
  • License MIT

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

Package Exports

  • @hiscojs/json-updater
  • @hiscojs/json-updater/dist/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@hiscojs/json-updater) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

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!