JSPM

@hiscojs/json-updater

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

Type-safe, immutable JSON updates with automatic formatting detection 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
  • 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)
}

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 }) => { ... }
});

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!