@hiscojs/json-updater
v1.1.0
Published
Type-safe, immutable JSON/JSONC updates with automatic formatting detection, comment preservation, and advanced array merging strategies
Maintainers
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!
