A Git-aware conflict resolver for JSON-first structured data.
- Git merge conflicts in structured files (JSON, YAML, XML, TOML) are painful.
- Manual resolution is error-prone, time-consuming, and breaks CI/CD pipelines.
git-json-resolver
automates conflict handling with configurable strategies.- Non-JSON formats are internally normalized to JSON → resolved → converted back.
- ⚡ Primary focus on JSON (first-class support)
- 🔄 YAML, XML, TOML, JSON5 supported via conversion
- 🧩 Rule-based strategies with path/pattern matching
- 📁 Multiple file parallel processing with include/exclude patterns
- 🔌 Pluggable matcher abstraction (picomatch, micromatch, or custom)
- 🛠️ CLI and programmatic API support
- 📝 Conflict sidecar files for unresolved conflicts
- 🔄 Backup and restore functionality
- 📊 Configurable logging (memory or file-based)
- 🔀 Git merge driver support for seamless Git integration
- 🔧 Plugin system for custom strategies with JSON config support
pnpm add git-json-resolver
or
npm install git-json-resolver
or
yarn add git-json-resolver
# Initialize config file
npx git-json-resolver --init
# Run with default config
npx git-json-resolver
# Run with options
npx git-json-resolver --include "**/*.json" --debug --sidecar
# Restore from backups
npx git-json-resolver --restore .merge-backups
Add a custom merge driver to your Git config:
git config merge.json-resolver.name "Custom JSON merge driver"
git config merge.json-resolver.driver "npx git-json-resolver %A %O %B"
Update .gitattributes
to use it for JSON files:
*.json merge=json-resolver
*.yaml merge=json-resolver
*.yml merge=json-resolver
*.toml merge=json-resolver
*.xml merge=json-resolver
How it works:
- Git automatically calls the merge driver during conflicts
- Uses same configuration and strategies as CLI mode
- Supports 3-way merge (ours, base, theirs)
- Returns proper exit codes (0 = success, 1 = conflicts)
import { resolveConflicts } from "git-json-resolver";
await resolveConflicts({
defaultStrategy: ["merge", "ours"],
rules: {
"dependencies.*": ["ours"],
version: ["theirs!"], // ! marks as important
"scripts.build": ["skip"],
},
include: ["**/*.json", "**/*.yaml"],
exclude: ["**/node_modules/**"],
matcher: "picomatch",
debug: true,
writeConflictSidecar: true,
backupDir: ".merge-backups",
});
JavaScript Config (git-json-resolver.config.js
)
module.exports = {
defaultStrategy: ["merge", "ours"],
rules: {
// Exact path matching
"package.json": {
version: ["theirs!"],
dependencies: ["ours"],
},
// Pattern matching
"*.config.json": {
"*": ["merge"],
},
},
// Alternative: byStrategy format
byStrategy: {
ours: ["dependencies.*", "devDependencies.*"],
"theirs!": ["version", "name"],
},
include: ["**/*.json", "**/*.yaml", "**/*.yml"],
exclude: ["**/node_modules/**", "**/dist/**"],
matcher: "picomatch",
debug: false,
writeConflictSidecar: false,
loggerConfig: {
mode: "memory", // or "stream"
logDir: "logs",
levels: {
stdout: ["warn", "error"],
file: ["info", "warn", "error"],
},
},
};
JSON Config (git-json-resolver.config.json
) -
{
"$schema": "https://cdn.jsdelivr.net/npm/git-json-resolver@latest/schema/config.schema.json",
"defaultStrategy": ["merge", "ours"],
"plugins": ["my-plugin"],
"pluginConfig": {
"my-plugin": {
"option": "value"
}
},
"rules": {
"package.json": {
"version": ["semantic-version", "theirs"],
"dependencies": ["ours"]
}
},
"byStrategy": {
"ours": ["dependencies.*", "devDependencies.*"],
"theirs!": ["version", "name"]
}
}
- No TypeScript intellisense for plugin strategies
- Limited validation for custom strategy names
- No compile-time type checking
- Recommended: Use
.js
or.ts
config for better developer experience
- merge → deep merge objects/arrays where possible
- ours → take current branch value
- theirs → take incoming branch value
- base → revert to common ancestor
- skip → leave unresolved (creates conflict entry)
- drop → remove the field entirely
- non-empty → prefer non-empty value (ours > theirs > base)
- update → update with theirs if field exists in ours
- concat → concatenate arrays from both sides
- unique → merge arrays and remove duplicates
- custom → user-defined resolver functions
- Strategies marked with
!
(important) are applied first - Multiple strategies can be specified as fallbacks
- Custom strategies can be defined via
customStrategies
config
- JSON (native)
- JSON5 → via
json5
peer dependency - YAML → via
yaml
peer dependency - TOML → via
smol-toml
peer dependency - XML → via
fast-xml-parser
peer dependency
All non-JSON formats are converted to JSON → resolved → converted back to original format.
# File patterns
--include "**/*.json,**/*.yaml" # Comma-separated patterns
--exclude "**/node_modules/**" # Exclusion patterns
# Matcher selection
--matcher picomatch # picomatch, micromatch, or custom
# Debug and logging
--debug # Enable verbose logging
--sidecar # Write conflict sidecar files
# Utilities
--init # Create starter config file
--restore .merge-backups # Restore from backup directory
- Modular design: Separate concerns (parsing, merging, serialization)
- Reusable utilities: Common merge logic extracted for maintainability
- Optimized bundle: Constants over enums for better minification
- Comprehensive testing: Full test coverage with vitest
- Type-safe: Full TypeScript support with proper type inference
- Exact paths:
"package.json"
,"src.config.database.host"
- Field matching:
"[version]"
→ matches anyversion
field - Glob patterns:
"dependencies.*"
,"**.config.**"
- Wildcards:
"*.json"
,"src/**/*.config.js"
For TypeScript/JavaScript configs, you can use either approach:
import { strategies } from "my-plugin";
// or import { semanticVersion, timestampLatest } from "my-plugin";
import { resolveConflicts } from "git-json-resolver";
await resolveConflicts({
customStrategies: {
...strategies,
// or "semantic-version": semanticVersion,
},
rules: {
version: ["semantic-version", "theirs"],
},
});
// Also works in .js/.ts configs
const config = {
plugins: ["my-plugin"],
pluginConfig: {
"my-plugin": { option: "value" },
},
rules: {
version: ["semantic-version", "theirs"],
},
};
{
"plugins": ["my-plugin"],
"pluginConfig": {
"my-plugin": { "option": "value" }
},
"rules": {
"version": ["semantic-version", "theirs"]
}
}
import type { Config } from "git-json-resolver";
const config: Config<AllStrategies | "semantic-version" | "timestamp-latest"> = {
// ... your config
};
Creating a Plugin
import { StrategyPlugin, StrategyStatus, StrategyFn } from "git-json-resolver";
// Augment types for TypeScript support
declare module "git-json-resolver" {
interface PluginStrategies {
"semantic-version": string;
"timestamp-latest": string;
}
}
// Individual strategy functions (can be imported directly)
export const semanticVersion: StrategyFn = ({ ours, theirs }) => {
if (isNewerVersion(theirs, ours)) {
return { status: StrategyStatus.OK, value: theirs };
}
return { status: StrategyStatus.CONTINUE };
};
export const timestampLatest: StrategyFn = ({ ours, theirs }) => {
const oursTime = new Date(ours as string).getTime();
const theirsTime = new Date(theirs as string).getTime();
return {
status: StrategyStatus.OK,
value: oursTime > theirsTime ? ours : theirs,
};
};
// Export strategies object for direct import
export const strategies = {
"semantic-version": semanticVersion,
"timestamp-latest": timestampLatest,
};
// Plugin interface for dynamic loading
const plugin: StrategyPlugin = {
strategies,
init: async config => {
console.log("Plugin initialized with:", config);
},
};
export default plugin;
import { StrategyStatus } from "git-json-resolver";
const config = {
customStrategies: {
"semantic-version": ({ ours, theirs }) => {
// Custom logic for semantic version resolution
if (isNewerVersion(theirs, ours)) {
return { status: StrategyStatus.OK, value: theirs };
}
return { status: StrategyStatus.CONTINUE };
},
},
rules: {
version: ["semantic-version", "theirs"],
},
};
- Memory mode: Fast, in-memory logging
- Stream mode: File-based logging for large operations
- Per-file logs: Separate log files for each processed file
- Debug mode: Detailed conflict information and strategy traces
See PLUGIN_GUIDE.md for detailed plugin development documentation.
Contributions welcome 🙌
- Fork, branch, PR — with tests (
vitest
required) - Docs live in
libDocs/
(tutorials, guides, deep dives) - API reference generated into
docs/
via TypeDoc
This library is licensed under the MPL-2.0 open-source license.
Please enroll in our courses or sponsor our work.
with 💖 by Mayank Kumar Chaudhari