The Swiss Army knife for nested JSON — traverse, flatten, search, validate, format, and diff JSON structures with a single lightweight Python library.
Working with deeply nested JSON in Python is painful. Standard dict access breaks on missing keys; jmespath has a custom query language to learn; pandas is overkill for simple lookups.
JSONavigator solves this with a zero-dependency, pure-Python code that covers the whole lifecycle of nested JSON manipulation — from traversal to diffing — in a handful of intuitive functions.
4,000+ downloads • Zero dependencies • Python 3.8+ • MIT Licensed
| Feature | Function | Description |
|---|---|---|
| 🔍 Traverse | traverse_json |
Recursively yield every (path, value) pair |
| 📦 Flatten | flatten_json |
Collapse nested JSON into a single-level dict |
| 🎯 Get by path | get_value_at_path |
Retrieve a value using a dot-notation path |
| ✍️ Set by path | set_value_at_path |
Update or set a value at a dot-notation path |
| 🗑️ Delete by path | delete_at_path |
Remove a key/index at a dot-notation path |
| 🧹 Delete key | delete_key |
Recursively remove every occurrence of a key |
| 🔎 Find value | find_value_of_element |
Search for first occurrence of a key, anywhere |
| 🗺️ Find all paths | find_all_paths_for_element |
Get every path where a key exists |
| 🧹 Clear values | empty_all_the_values |
Reset all leaf values to "" in-place |
| ✅ Validate path | validate_path |
Assert a path string is well-formed |
| 💅 Format path | format_path |
Pretty-print a path for human readability |
| 📊 Compare JSON | compare_files |
Diff two JSON objects or files with a summary |
| 🚨 Custom exceptions | InvalidPathError, ElementNotFoundError |
Semantic error handling |
pip install jsonavigatorFrom source:
git clone https://github.com/Nikhil-Singh-2503/JSONavigator.git
cd JSONavigator
python -m venv venv && source venv/bin/activate
pip install -r requirements.txtRequires: Python ≥ 3.8. No runtime dependencies.
from jsoninja import (
traverse_json,
get_value_at_path,
flatten_json,
find_value_of_element,
find_all_paths_for_element,
empty_all_the_values,
validate_path,
format_path,
compare_files,
)
data = {
"user": {
"name": "Alice",
"scores": [95, 87, 100],
"address": {"city": "Berlin", "zip": "10115"}
}
}
# Traverse every leaf
for path, value in traverse_json(data):
print(f"{path}: {value}")
# user.name: Alice
# user.scores[0]: 95 ...
# Retrieve by path
print(get_value_at_path(data, "user.address.city")) # Berlin
# Flatten to single level
flat = flatten_json(data)
# {"user.name": "Alice", "user.scores[0]": 95, ...}View Details
Yields (path, value) tuples for every leaf node. Supports mixed dicts and lists.
from jsoninja import traverse_json
data = {"a": {"b": [1, 2], "c": 3}}
for path, value in traverse_json(data):
print(f"Path: {path}, Value: {value}")Path: a.b[0], Value: 1
Path: a.b[1], Value: 2
Path: a.c, Value: 3
Use the
separatorparameter to change the default.delimiter.traverse_json(data, separator="/") # a/b[0], a/b[1], a/c
View Details
from jsoninja import get_value_at_path
data = {"a": {"b": [10, 20, 30]}}
print(get_value_at_path(data, "a.b[2]")) # 30
print(get_value_at_path(data, "a.b[0]")) # 10View Details
Update or create a leaf value using dot notation, modifying the dict in place.
from jsoninja import set_value_at_path
data = {"user": {"scores": [10, 20]}}
set_value_at_path(data, "user.scores[1]", 99)
print(data) # {"user": {"scores": [10, 99]}}View Details
Delete a key or pop an index at the specified path.
from jsoninja import delete_at_path
data = {"a": {"b": 1, "c": 2}}
delete_at_path(data, "a.b")
print(data) # {"a": {"c": 2}}View Details
Collapses any depth of nesting into a flat {path: value} dictionary.
from jsoninja import flatten_json
data = {"a": {"b": [1, 2], "c": 3}}
print(flatten_json(data)){
"a.b[0]": 1,
"a.b[1]": 2,
"a.c": 3
}View Details
Returns the first occurrence of a key anywhere in the structure.
from jsoninja import find_value_of_element
data = {"a": {"b": {"c": 42}}}
print(find_value_of_element("c", data)) # 42
print(find_value_of_element("z", data)) # "" (not found)View Details
Returns every path where the target key appears — great for duplicate-key analysis.
from jsoninja import find_all_paths_for_element
data = {"a": 1, "b": {"a": 2}, "c": [{"a": 3}]}
print(find_all_paths_for_element(data, "a"))
# ["a", "b.a", "c[0].a"]View Details
Resets every leaf value to "" — useful for creating JSON templates or anonymising data.
from jsoninja import empty_all_the_values
data = {
"a": 1,
"b": {"c": 42, "d": [1, 2, {"e": "hello"}]},
}
print(empty_all_the_values(data)){
"a": "",
"b": {"c": "", "d": ["", "", {"e": ""}]}
}View Details
Recursively remove every occurrence of a key name anywhere in the nested object.
from jsoninja import delete_key
data = {"id": 1, "user": {"id": 2, "name": "Alice"}}
delete_key(data, "id")
print(data) # {"user": {"name": "Alice"}}View Details
from jsoninja import validate_path, format_path
from jsoninja.exceptions import InvalidPathError
# Validation
try:
validate_path("a.b[1]") # returns True
validate_path("a.b[1") # raises InvalidPathError (mismatched brackets)
except InvalidPathError as e:
print(f"Error: {e}")
# Human-readable formatting
print(format_path("user.address.city")) # user -> address -> city
print(format_path("a.b[1]")) # a -> b[1]View Details
Diff two JSON objects or files and receive a structured list of changes plus a summary.
from jsoninja import compare_files
json1 = {"a": {"b": 1, "c": 2}, "d": [1, 2]}
json2 = {"a": {"b": 1, "c": 99}, "d": [1, 2, 3], "e": "new"}
changes, summary = compare_files(json1, json2)
print(summary)
# {"added": 2, "removed": 0, "changed": 1, "type_changed": 0, "unknown": 0}
for change in changes:
print(change)
# {"type": "changed", "path": "a-->c", "old_value": 2, "new_value": 99}
# {"type": "added", "path": "d[2]", "new_value": 3}
# {"type": "added", "path": "e", "new_value": "new"}Compare from file paths:
changes, summary = compare_files(
r"path/to/old.json",
r"path/to/new.json",
isPath=True
)Note: When passing file paths as strings, use raw strings (
r"...") or double backslashes (\\) to avoid escape-sequence issues.
Change types returned:
| Type | Meaning |
|---|---|
added |
Key/value exists in JSON 2 but not JSON 1 |
removed |
Key/value exists in JSON 1 but not JSON 2 |
changed |
Same path, different value |
type_changed |
Same path, different Python type |
JSONavigator/
├── jsoninja/
│ ├── __init__.py # Public API surface
│ ├── core.py # traverse, get, find, empty functions
│ ├── utils.py # flatten, validate, format helpers
│ ├── compare.py # JSON diff engine
│ └── exceptions.py # Custom exception hierarchy
├── tests/
│ ├── test_core.py # 30+ core function tests
│ ├── test_compare.py # Compare module tests
│ ├── test_utils.py # Utility function tests
│ └── conftest.py # pytest fixtures
├── .github/workflows/
│ └── publish-to-pypi.yml # Automated PyPI release CI
├── requirements.txt # Dev/test dependencies
├── setup.py
└── README.md
| Function | Signature | Returns |
|---|---|---|
traverse_json |
(data, parent_key='', separator='.') |
Generator of (path, value) |
get_value_at_path |
(data, path, separator='.') |
Value at the path |
set_value_at_path |
(data, path, new_value, separator='.') |
Mutated data |
delete_at_path |
(data, path, separator='.') |
Mutated data |
delete_key |
(data, target_key) |
Mutated data |
find_value_of_element |
(target_key, data) |
First matched value or "" |
find_all_paths_for_element |
(file_data, target_key, path='', separator='.') |
list[str] of all paths |
empty_all_the_values |
(data) |
Mutated data with "" leaves, or None |
| Function | Signature | Returns |
|---|---|---|
flatten_json |
(data, parent_key='', separator='.') |
Flat dict |
validate_path |
(path, separator='.') |
True or raises InvalidPathError |
format_path |
(path, separator='.') |
Human-readable path string |
| Function | Signature | Returns |
|---|---|---|
compare_files |
(file1, file2, separator='-->', isPath=False) |
(changes_list, summary_dict) |
| Exception | When raised |
|---|---|
InvalidPathError |
Malformed path string passed to validate_path / format_path |
ElementNotFoundError |
Target element absent from JSON structure |
JSONStructureError |
Unexpected JSON structure type |
- 🔬 API response inspection — quickly explore the shape of deeply nested API payloads.
- 🧪 Test assertion helpers — flatten and validate JSON responses in pytest fixtures.
- 📋 Config file diffing — compare application config files between environments.
- 🏗️ JSON schema generation — traverse and collect all paths to infer structure.
- 🕵️ Data anonymisation —
empty_all_the_valuesstrips sensitive data for safe sharing. - 📥 ETL pipelines — flatten nested JSON before inserting into relational databases.
# 1. Clone the repository
git clone https://github.com/Nikhil-Singh-2503/JSONavigator.git
cd JSONavigator
# 2. Create and activate a virtual environment
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# 3. Install development dependencies
pip install -r requirements.txt
# 4. Run the tests
pytest
# 5. Run with coverage report
pytest --cov=jsoninja --cov-report=term-missingThe test suite covers all public functions with 30+ unit tests using pytest.
# All tests
pytest
# Specific module
pytest tests/test_core.py
pytest tests/test_compare.py
pytest tests/test_utils.py
# With HTML coverage report
pytest --cov=jsoninja --cov-report=htmlContributions are warmly welcome! Here's how to get started:
- Fork the repository on GitHub.
- Clone your fork:
git clone https://github.com/<your-username>/JSONavigator.git
- Create a feature branch:
git checkout -b feature/my-feature
- Make changes and add tests for any new functionality.
- Run tests to make sure everything passes:
pytest
- Commit and push your changes:
git commit -m "feat: add my-feature" git push origin feature/my-feature - Open a Pull Request against the
mainbranch.
Please follow the existing code style and write docstrings for any new functions.
-
set_value_at_path— update a value at a given path in place -
delete_key— remove a key anywhere in the structure - JSONPath (
$) query syntax support - Async-friendly streaming traversal for large JSON files
- Type-annotated stubs (
py.typedmarker) -
compare_files— side-by-side HTML diff report
This project is licensed under the MIT License — see the LICENSE file for details.
Nikhil Singh
- 📧 Email: nikhilraj7654@gmail.com
- 🐙 GitHub: @Nikhil-Singh-2503
- 📦 PyPI: jsonavigator
If JSONavigator saves you time, please consider giving it a ⭐ on GitHub — it helps others discover the project!