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-updaterQuick 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 preservedJSONC 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 itemsDependencies
- @hiscojs/object-updater: Core object manipulation
License
MIT
Repository
https://github.com/hisco/json-updater
Contributing
Issues and pull requests welcome!