JSON Merge Conflict Error: Resolving Git Markers and Key Collisions
Quick answer
💡Git merge conflicts insert plain-text markers like <<<<<<< HEAD, =======, and >>>>>>> branch-name directly into the conflicted file. JSON has no comment syntax, so these markers are treated as literal content that violates JSON syntax. JSON.parse throws SyntaxError: Unexpected token < on the first angle bracket. Resolve the conflict markers in your editor or with git mergetool before attempting to parse or validate the file.
Error symptoms
- ✕
SyntaxError: Unexpected token < in JSON at position N - ✕
json.decoder.JSONDecodeError: Expecting value: line N column 1 - ✕
jq parse error: Invalid numeric literal at line N - ✕
CI pipeline fails with JSON parse error immediately after a merge - ✕
package.json or tsconfig.json rejected by npm or tsc after merging branches - ✕
Application startup fails with configuration parse error after a git merge
Common causes
- •Two branches both modified the same JSON file and git could not auto-merge
- •package-lock.json or yarn.lock conflicts carried over after resolving package.json
- •Both branches added the same key with different values causing a key collision
- •A rebase operation left conflict markers in configuration files
- •Merge tool resolved source files but left the generated JSON files unresolved
- •Shallow merge with Object.assign overwrote nested objects instead of merging them
When it happens
- •Merging feature branches that both added dependencies to package.json
- •Rebasing a long-lived branch onto main after another developer changed config files
- •Cherry-picking commits that touched shared configuration or translation JSON files
- •Resolving conflicts in translation files where both branches added new string keys
- •After automated dependency updates (Dependabot or Renovate) conflict with manual changes
Examples and fixes
A tsconfig.json with unresolved merge conflict markers causes tsc to fail with a JSON parse error.
Removing Git conflict markers from a JSON config file
❌ Wrong
{
"compilerOptions": {
<<<<<<< HEAD
"target": "ES2020",
"strict": true
=======
"target": "ES2022",
"strict": true,
"noUncheckedIndexedAccess": true
>>>>>>> feature/strict-mode
}
}✅ Fixed
{
"compilerOptions": {
"target": "ES2022",
"strict": true,
"noUncheckedIndexedAccess": true
}
}The conflict markers <<<<<<< HEAD, =======, and >>>>>>> are not valid JSON syntax. They appear as literal text that the parser tries to read as a key or value, immediately failing on the angle bracket character. After resolving the conflict, choose the desired values from each section. In this case, take the ES2022 target and the noUncheckedIndexedAccess option from the feature branch, keeping strict true from both branches. Remove all three marker lines and verify the resulting JSON is valid with /tools/json-validator before committing.
Using Object.assign for a shallow merge loses nested properties from the source object.
Merging two JSON objects without overwriting nested values
❌ Wrong
const base = {
server: { port: 3000, host: 'localhost' },
logging: { level: 'info' }
};
const patch = {
server: { port: 8080 },
logging: { format: 'json' }
};
// Shallow merge replaces entire nested objects
const config = Object.assign({}, base, patch);
// server.host is undefined, logging.level is gone✅ Fixed
const base = {
server: { port: 3000, host: 'localhost' },
logging: { level: 'info' }
};
const patch = {
server: { port: 8080 },
logging: { format: 'json' }
};
// Deep merge preserves nested properties from both
const { merge } = require('lodash');
const config = merge({}, base, patch);
// { server: { port: 8080, host: 'localhost' },
// logging: { level: 'info', format: 'json' } }Object.assign performs a shallow merge, copying only top-level properties. When a top-level property is itself an object, the entire nested object from the later source replaces the one from earlier sources. The server object from patch replaces the server object from base, losing the host key entirely. lodash merge recursively merges nested objects property by property, preserving keys from both sources at every nesting level. For HTTP PATCH semantics, the npm package json-merge-patch implements RFC 7396, where null values in the patch document delete the corresponding key in the target.
Why Git conflict markers break JSON
Git's conflict resolution system marks conflicting sections of text files with three-line delimiters: <<<<<<< HEAD marks the start of the current branch's version, ======= separates the two versions, and >>>>>>> branch-name marks the end of the incoming version. This convention works for programming language source files because those languages typically have comment syntax that can contain arbitrary text. JSON has no comment syntax whatsoever, not even single-line comments.
When Git inserts <<<<<<< HEAD into a JSON file, the parser reads the first < character and immediately throws a SyntaxError because < is not a valid start character for any JSON value type. Valid JSON values begin with a double quote for strings, a digit or minus sign for numbers, t for true, f for false, n for null, an opening brace for objects, or an opening bracket for arrays. The angle bracket has no valid interpretation, so parsers fail fast at that point.
JSON files are especially prone to merge conflicts because their structure is rigid. In a JavaScript source file, two developers can add functions in different parts of the file and Git can often auto-merge by interleaving the additions. In a JSON file, nearly every addition to a nested object requires modifying the same region of the file: adding a comma after the previous key, adding the new key-value pair, and potentially adjusting the closing brace. These surrounding formatting changes cause conflicts that Git cannot resolve automatically.
The most conflict-prone JSON files in practice are package.json and its lock files. When two developers add different npm packages in their respective branches, both modified the dependencies object and its surrounding commas. Git flags the entire region as conflicted even when the actual intent is simply to add both packages. package-lock.json conflicts are even more severe because the lock file contains hashes and resolved URLs for every transitive dependency, and two different package installs produce completely different structures for overlapping package trees.
Translation and i18n JSON files accumulate conflicts frequently because multiple developers adding new string keys in parallel branches will conflict whenever their additions fall alphabetically near each other in a sorted file. The JSON content itself has no semantic conflict — both keys should exist in the final file — but Git sees overlapping line ranges and marks the region as needing manual resolution.
Finding conflict markers after a failed merge
After a merge or rebase operation, Git marks conflicted files but does not always make them immediately visible, especially when the conflict is in a generated file like package-lock.json that developers rarely open directly. The fastest diagnostic is to run git diff --name-only --diff-filter=U from the repository root to list all files currently marked as unresolved by Git. Every file in this list contains conflict markers that must be removed before the merge can be committed.
To scan all JSON files for conflict markers without relying on Git's tracking, use grep -r '<<<<<<<' --include='*.json' . from the repository root. This catches cases where a developer mistakenly staged the conflicted file or where the file was modified outside of Git's conflict tracking. A single match anywhere in any JSON file means that file is invalid and will fail to parse at runtime.
For individual files, /tools/json-validator will immediately surface the error position. The error message typically points to the line and character of the conflict marker line. If the validator shows Unexpected token < at the start of a line, search the file for <<<<<<< to find the conflict region. The angle bracket character is distinctive enough to locate with a simple text search even in large files.
In VS Code, files with unresolved conflicts show a decorative warning indicator in the file tree. The editor highlights conflict regions with colored backgrounds — typically green for the current branch changes and blue for the incoming changes — and provides Accept Current Change, Accept Incoming Change, Accept Both Changes, and Compare Changes buttons. These automate the mechanical work of removing the conflict markers while preserving the desired content.
For Python codebases, json.loads raises JSONDecodeError with lineno and colno attributes that point directly to the conflict marker line. A quick diagnostic script using try/except around json.load() on every JSON file in a directory can identify all broken files before deployment. The Python json module does not attempt recovery from syntax errors, so the first conflict marker terminates parsing immediately and the error position is reliable.
Resolving JSON conflicts correctly
The safest resolution method for JSON merge conflicts is to use a merge tool that understands file structure rather than treating the file as plain text. VS Code's built-in merge editor shows both versions side by side and lets you click to accept individual hunks. For terminal users, git mergetool with vimdiff or the Meld GUI tool achieves the same result. IntelliJ-based editors have an excellent three-way merge view showing the base version alongside both branches, which makes it clear what each branch changed relative to the common ancestor.
For package.json conflicts specifically, the recommended workflow is to accept changes from both branches in the dependencies section, ensure both sets of package names are present in the resolved file, and then run npm install to regenerate package-lock.json. Never manually merge package-lock.json. The lock file contains SHA-512 integrity hashes computed from actual package tarball bytes, and a manually merged lock file has incorrect hashes that cause npm ci to fail with integrity check errors in every environment that uses strict lock file validation.
For configuration JSON files, the resolution depends on the semantic intent of each conflict. If both branches changed the same key to different values, you must choose one or reconcile them by understanding what each branch needed. If both branches added different keys to the same object, accept both additions by keeping all keys from both sections. The most costly mistake is clicking Accept Current and discarding incoming changes without reading what was changed.
The jq tool provides programmatic merge for JSON files when the merge strategy is known in advance. For a simple object merge where the patch should override the base: jq -s '.[0] * .[1]' base.json patch.json outputs the merged result. The -s flag slurps both files into an array, and the * operator performs a recursive merge of the two objects. Redirect the output to a new file and validate it with /tools/json-validator before using it in production.
For HTTP PATCH semantics, the json-merge-patch npm package implements RFC 7396. This RFC defines that null values in the patch document delete the corresponding key in the target, while all other values overwrite. This standard is appropriate for REST APIs supporting partial resource updates and avoids the ambiguity of what merging arrays should mean in a general context.
Tricky scenarios in JSON conflict resolution
Key collisions are a subtler form of merge conflict that Git sometimes auto-resolves incorrectly. If the base version of a JSON file has a key timeout with value 5000, and both branches change it to different values, Git marks this as a conflict. But if one branch adds a new key that did not exist in the other branch, Git may auto-merge by adding the key without flagging a conflict, even when the key now logically conflicts with assumptions in the other branch. Most JSON parsers silently use the last occurrence of a duplicate key and discard earlier ones, which can create subtle configuration bugs.
Automated dependency update bots like Dependabot and Renovate create branches that modify package.json and package-lock.json. When multiple bot PRs are open simultaneously and one is merged, the remaining PRs conflict. The bot typically rebases automatically, but the rebase may produce an incorrect lock file if the npm registry state changed between the original bot branch creation and the rebase attempt. Always run npm install after merging any dependency update and verify the lock file reflects the expected package versions.
JSON files used as test fixtures accumulate conflicts frequently when multiple developers add test cases in parallel. A useful alternative is to store test fixtures as JavaScript modules that export objects rather than as JSON files. JavaScript modules support the spread operator and comments, making them dramatically easier to merge. Convert to JSON at build time if the test runner requires strict JSON format. This one architectural change eliminates an entire category of repetitive conflict resolution work.
Translation files with thousands of keys are painful to merge manually at scale. Tools like i18next-scanner regenerate translation keys from source code, which transforms the merge problem from a text conflict into a code change. After resolving source code conflicts, re-run the scanner to produce a clean translation file with all keys from both branches, eliminating the need to manually merge the JSON translation file itself.
Deep nesting makes conflict regions harder to understand because a single key change requires surrounding comma adjustments across multiple indented levels. Flattening JSON configuration files to a single level of nesting using dot-notation keys, such as server.port instead of nested objects, reduces conflict surface area. Each logical value occupies exactly one line, and Git can auto-merge most additions without conflicts.
Mistakes developers make resolving JSON conflicts
Accepting the entire current branch version without reading the incoming changes is the most expensive mistake in conflict resolution. It discards committed work from the other branch and introduces a bug that may not surface until the discarded feature is explicitly tested weeks later. Every conflict resolution requires reading both versions and understanding the intent of each change before deciding on a resolution.
Manually editing package-lock.json to resolve conflicts produces a lock file with incorrect integrity hashes. The SHA-512 hashes in package-lock.json are computed from the actual package tarball bytes at the time of download. When you manually copy a hash from one branch or edit the resolved URL to match your preferred version, the hash no longer matches the package that npm will download. This causes npm ci to fail with integrity check errors, breaking CI pipelines in every environment that uses the lock file for reproducible installs.
Forgetting to validate the resolved JSON before committing is another frequent mistake. After removing conflict markers and choosing values, the resulting JSON may still have syntax errors introduced during editing: a missing comma between the last resolved key and the next key, a trailing comma before a closing brace, or mismatched brackets from a partial accept. Run the resolved file through /tools/json-validator or jq '.' resolved.json before staging it with git add.
Using a shallow merge (Object.assign or the spread operator) when applying configuration patches loses nested properties silently. This is common in application startup code that merges default configuration with environment-specific overrides. When a developer adds a new nested key to the defaults and another developer applies an environment override written before that key existed, the spread operator replaces the entire nested object and removes the new default without any warning or error.
Assuming that git merge --strategy-option=theirs resolves JSON conflicts correctly will silently discard all changes from one entire branch in every conflicted file, not just the specifically conflicted region. The ours and theirs strategies accept one entire file version wholesale. Use them only when you are certain that one entire file version is correct and the other should be completely abandoned, which is rarely the case in practice.
Preventing JSON merge conflicts in team workflows
Split large monolithic JSON files into smaller files organized by domain or feature. A single translations.json with 2000 keys will conflict whenever two developers add translations simultaneously. Splitting into translations/auth.json, translations/dashboard.json, and translations/settings.json reduces the probability that two developers edit the same file in the same sprint. Most frameworks support loading multiple translation files and merging them at startup.
Keep JSON configuration files minimal, with defaults defined in application code rather than in JSON files. Every key in a JSON config file is a potential conflict point. If a key has a sensible default that applies in most environments, define it in code and put only environment-specific overrides in the JSON file. Fewer keys mean fewer conflicts and fewer decisions to make during resolution.
For lock files, configure CI to run npm ci rather than npm install. npm ci strictly validates that the lock file matches package.json and fails immediately if they are inconsistent, catching incorrect manual merges before they reach production. Set up a separate CI check that runs npm install and fails if the lock file is modified, ensuring the lock file is always freshly generated rather than hand-edited or improperly merged.
Use semantic conflict resolution in your team's merge workflow. For any JSON file representing configuration or data, write a brief note in the PR description describing what values were changed and why. When a merge conflict occurs, the resolver can read both PR descriptions to understand the intent of each change rather than guessing from raw values. This is particularly important for numeric settings like timeout values or retry counts where choosing between two numbers requires understanding the performance characteristics of each environment.
Consider using JSON Schema validation in pre-commit hooks. A hook that runs ajv or another validator against all modified JSON files catches structural errors introduced during conflict resolution before the commit is created. Format JSON files consistently with Prettier to minimize diff noise and reduce the number of lines that Git considers changed during a merge. Consistently formatted files produce smaller diffs, which means smaller conflict regions and fewer manual resolution decisions.
Quick fix checklist
- ✓Run git diff --name-only --diff-filter=U to list all unresolved conflict files
- ✓Search for <<<<<<< in all JSON files with grep -r '<<<<<<<' --include='*.json' .
- ✓For package.json: resolve conflict, delete package-lock.json, run npm install
- ✓For config files: read both conflict sections before choosing a resolution
- ✓Validate the resolved file with /tools/json-validator before git add
- ✓Use lodash merge instead of Object.assign for nested configuration merges
- ✓Set up a pre-commit hook that validates all modified JSON files
- ✓Enable git rerere with git config --global rerere.enabled true to auto-replay resolutions
Related guides
Frequently asked questions
Why does SyntaxError: Unexpected token < appear after a git merge?
Git inserts conflict markers starting with <<<<<<< HEAD into any file it cannot automatically merge. JSON parsers read the first < character and immediately throw a SyntaxError because < is not a valid JSON value character. The error appears at the line number of the first conflict marker. Search for <<<<<<< in the file to locate all conflict regions that need manual resolution before the file will parse successfully.
How do I resolve package-lock.json merge conflicts correctly?
Delete package-lock.json entirely and run npm install to regenerate it. Never manually merge package-lock.json because it contains SHA-512 integrity hashes computed from actual package tarbytes. A manually merged lock file has incorrect hashes that cause npm ci to fail with integrity check errors. First resolve package.json by keeping all dependency entries from both branches, then delete the lock file and run npm install to produce a correct regenerated version.
What is the difference between shallow and deep JSON merge?
A shallow merge using Object.assign or the spread operator copies only top-level properties. When a property is itself an object, the entire nested object from the later source replaces the earlier one, losing nested keys from the base. A deep merge using lodash merge recursively combines nested objects at every level, preserving keys from both sources. Use deep merge for configuration objects where both sources may add different nested properties.
Can jq merge two JSON files programmatically?
Yes. Use jq -s '.[0] * .[1]' base.json patch.json to merge two JSON object files. The -s flag slurps both files into an array, and the * operator performs a recursive merge. Properties from the second file override those from the first at each level. For arrays, * concatenates rather than merging by index. Redirect output to a new file and validate it with /tools/json-validator before using it in production.
What is json-merge-patch and when should I use it?
json-merge-patch implements RFC 7396, a standard for HTTP PATCH operations on JSON resources. A patch document specifies which properties to change, with null values indicating that the corresponding key should be deleted from the target. This is simpler than JSON Patch RFC 6902 for most partial update scenarios. Use it when building REST APIs supporting partial updates of JSON resources, or for applying configuration overrides where null should remove a key entirely.
How do I prevent merge conflicts in translation JSON files?
Split large translation files by feature domain so different teams modify different files. Use i18next-scanner or a similar tool to generate translation keys from source code rather than manually editing JSON files. This turns the merge problem into a source code conflict, which is easier to resolve. Configure Prettier to sort JSON keys alphabetically and use consistent formatting to produce smaller diffs and reduce the number of lines that Git marks as conflicted.
Does json.loads in Python give useful error messages for conflict markers?
Yes. Python's json.loads raises JSONDecodeError with a message like Expecting value: line N column 1. The lineno and colno attributes on the exception point directly to the conflict marker line. The Python json module fails immediately at the first conflict marker without attempting recovery, so the error position is reliable and accurately identifies where the first <<<<<<< marker appears in the file.
Can git rerere help avoid repeating the same JSON conflict resolution?
Yes. Enable with git config --global rerere.enabled true. Git records each manual conflict resolution and replays it automatically when the same conflict pattern appears again, which happens frequently with long-lived feature branches that require periodic rebasing. For JSON files with stable recurring conflict patterns like translation keys, rerere eliminates most repeated manual work by recognizing and automatically applying previously recorded resolutions.
All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-06.