Skip to content

JSON Merge

The mergeJson function performs a deep merge of two JavaScript objects using the defu library. It’s used whenever xtarterize needs to modify JSON configuration files like tsconfig.json, biome.json, or .vscode/settings.json.

import { mergeJson } from '@xtarterize/patchers'
const existing = { compilerOptions: { strict: true, target: "ES2022" } }
const incoming = { compilerOptions: { incremental: true } }
const merged = mergeJson(existing, incoming)
// { compilerOptions: { strict: true, target: "ES2022", incremental: true } }
Parameter Type Description
existing object The current configuration (takes precedence)
incoming object The new configuration to merge in (fills gaps)

Returns the merged object. Existing keys always take precedence over incoming keys.

Nested objects are merged recursively. Existing keys take precedence, missing keys are filled from incoming:

const existing = {
compilerOptions: {
strict: true, // https://www.typescriptlang.org/tsconfig/#strict
target: "ES2022" // https://www.typescriptlang.org/tsconfig/#target
}
}
const incoming = {
compilerOptions: {
incremental: true, // https://www.typescriptlang.org/tsconfig/#incremental
tsBuildInfoFile: ".tsbuildinfo" // https://www.typescriptlang.org/tsconfig/#tsBuildInfoFile
}
}
const merged = mergeJson(existing, incoming)
// {
// compilerOptions: {
// strict: true, // preserved from existing
// target: "ES2022", // preserved from existing
// incremental: true, // added from incoming
// tsBuildInfoFile: ".tsbuildinfo" // added from incoming
// }
// }
Type Behavior Example
Objects Deep merge, existing keys win { a: { b: 1 } } + { a: { c: 2 } }{ a: { b: 1, c: 2 } }
Arrays Replace entirely { rules: ["a"] } + { rules: ["b"] }{ rules: ["b"] }
flowchart TD
E[Existing config] --> M{mergeJson}
I[Incoming config] --> M
M --> P{Key exists?}
P -->|Yes| K[Keep existing value]
P -->|No| A[Add incoming value]
P -->|Nested object| R[Recurse merge]
K --> O[Merged result]
A --> O
R --> O
style E fill:#6366f1,color:#fff
style I fill:#f59e0b,color:#fff
style O fill:#22c55e,color:#fff
const existing = { plugins: ['pluginA'] }
const incoming = { plugins: ['pluginB'] }
const merged = mergeJson(existing, incoming)
// { plugins: ['pluginB'] } — incoming replaces existing array

For cases where arrays need to be combined (like VS Code extension recommendations), use a custom merge function instead of mergeJson.

While mergeJson operates on objects, patchJson edits JSON text directly using jsonc-parser. It preserves:

  • Existing comments (// inline and /* block */)
  • Key ordering
  • Whitespace and indentation style
  • Trailing commas (in JSONC)
import { patchJson } from '@xtarterize/patchers'
const existing = `{
// Keep this comment
"strict": true,
"target": "ES2022"
}`
const incoming = { compilerOptions: { incremental: true } }
const result = patchJson(existing, incoming)
// {
// // Keep this comment
// "strict": true,
// "target": "ES2022",
// "incremental": true
// }
Function Use When Preservation
mergeJson Object-level logic, deep merging None (returns new object)
patchJson Writing changes back to a file Comments, formatting, key order

createJsonMergeTask and createMultiFileJsonMergeTask use both: mergeJson computes the target state, then patchJson applies it surgically to the original text. In @xtarterize/tasks, this shared behavior is centralized in factory-config.ts.

// Existing
const existing = { compilerOptions: { strict: true, target: "ES2022" } }
// Incoming: add incremental builds
const incoming = { compilerOptions: { incremental: true } }
// Result: both preserved