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-resolverautomates 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-resolveror
npm install git-json-resolveror
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-backupsAdd 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-resolverHow 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
.jsor.tsconfig 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
customStrategiesconfig
- JSON (native)
- JSON5 → via
json5peer dependency - YAML → via
yamlpeer dependency - TOML → via
smol-tomlpeer dependency - XML → via
fast-xml-parserpeer 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 anyversionfield - 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 (object-based) const plugin: StrategyPlugin = { strategies, init: async config => { console.log("Plugin initialized with:", config); }, }; export default plugin; // Alternative: Function-based plugin (NEW) export default async function createPlugin(config?: any): Promise<StrategyPlugin> { // Initialize with config if needed console.log("Plugin initialized with:", config); return { strategies, init: async initConfig => { // Additional initialization if needed }, }; }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 (
vitestrequired) - 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

