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 } }
ParameterTypeDescription
existingobjectThe current configuration (takes precedence)
incomingobjectThe 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
// }
// }
TypeBehaviorExample
ObjectsDeep merge, existing keys win{ a: { b: 1 } } + { a: { c: 2 } }{ a: { b: 1, c: 2 } }
ArraysReplace 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
// }
FunctionUse WhenPreservation
mergeJsonObject-level logic, deep mergingNone (returns new object)
patchJsonWriting changes back to a fileComments, 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