Simple, safe and dependency-free JSON utility library for automation frameworks and Python applications.
Project description
ouroboros_json
Simple, safe and dependency-free JSON utility library for Python.
ouroboros_json provides a thin, well-typed wrapper around Python's standard json module with pragmatic helpers for automation frameworks, test suites and small applications.
Features
| Area | Capabilities |
|---|---|
| I/O | Load, write and safe-default reads with automatic parent-directory creation |
| Key / Value | Add, extract, check and remove top-level keys from JSON files |
| Nested Access | Dot-notation get, set, has and delete on arbitrarily deep dicts |
| Transform | Deep merge, flatten / unflatten, filter / exclude, structural diff, recursive key sort |
| Comparison | Structural subset comparison between JSON objects |
| Serialization | String encode / decode, validity check, pretty-print |
| Matrix | 2-D matrix read & write helpers |
| Introspection | List keys (flat or nested), count keys, find values by predicate |
| Typed | PEP 561 py.typed marker, JSONType and PathLike aliases |
| Zero deps | Only Python stdlib |
Installation
pip install ouroboros_json
Python Compatibility
Supported: Python 3.9 - 3.14
Architecture
ouroboros_json/
+-- __init__.py # Public API exports + __version__
+-- _types.py # JSONType, PathLike aliases + internal helpers
+-- _io.py # IOMixin: load_json, write_json, load_or_default
+-- _keyvalue.py # KeyValueMixin: add, extract, check, remove keys
+-- _nested.py # NestedMixin: get/set/has/delete via dot-notation
+-- _transform.py # TransformMixin: merge, flatten, compare, diff, sort, find
+-- _serialization.py # SerializationMixin: to/from string, matrix, pretty-print
+-- base.py # BaseJSON facade (composes all mixins)
+-- exceptions.py # Full exception hierarchy
+-- py.typed # PEP 561 typed marker
tests/
+-- conftest.py # Shared fixtures
+-- test_io.py # I/O tests
+-- test_keyvalue.py # Key/value tests
+-- test_nested.py # Nested access tests
+-- test_transform.py # Transform tests
+-- test_serialization.py # Serialization tests
+-- test_base.py # Facade / integration tests
Quick Start
from ouroboros_json import BaseJSON
# Load / write files
data = BaseJSON.load_json("config.json")
BaseJSON.write_json("output.json", {"status": "ok"})
cfg = BaseJSON.load_or_default("optional.json", default={})
# Add or update a key in a file
BaseJSON.add_value_to_json("./tmp/data.json", "env", "dev")
# Extract a value
branch = BaseJSON.extract_value_from_json("./tmp/data.json", "branch")
# Check if a value exists
has_token = BaseJSON.check_value_in_json("./tmp/data.json", value="abc123")
Nested Access (dot-notation)
cfg = {"db": {"host": "localhost", "port": 5432}}
# Get
host = BaseJSON.get_nested(cfg, "db.host") # "localhost"
name = BaseJSON.get_nested(cfg, "db.name", None) # None (default)
# Set (creates intermediates automatically)
d: dict = {}
BaseJSON.set_nested(d, "db.host", "localhost")
# d == {"db": {"host": "localhost"}}
# Check existence
BaseJSON.has_nested(cfg, "db.host") # True
BaseJSON.has_nested(cfg, "db.name") # False
# Delete
d = {"a": {"b": 1, "c": 2}}
BaseJSON.delete_nested(d, "a.b")
# d == {"a": {"c": 2}}
# Custom separator
BaseJSON.get_nested(cfg, "db/host", separator="/")
Deep Merge
base = {"db": {"host": "localhost", "port": 5432}}
over = {"db": {"port": 3306}, "debug": True}
merged = BaseJSON.deep_merge(base, over)
# {"db": {"host": "localhost", "port": 3306}, "debug": True}
Flatten / Unflatten
flat = BaseJSON.flatten({"a": {"b": 1, "c": {"d": 2}}})
# {"a.b": 1, "a.c.d": 2}
nested = BaseJSON.unflatten({"a.b": 1, "a.c.d": 2})
# {"a": {"b": 1, "c": {"d": 2}}}
Diff
a = {"x": 1, "y": 2, "z": 3}
b = {"x": 1, "y": 99, "w": 4}
result = BaseJSON.diff(a, b)
# {"added": ["w"], "removed": ["z"], "changed": ["y"]}
Filter / Exclude / Remove
BaseJSON.filter_keys({"a": 1, "b": 2, "c": 3}, ["a", "c"]) # {"a": 1, "c": 3}
BaseJSON.exclude_keys({"a": 1, "b": 2, "c": 3}, ["b"]) # {"a": 1, "c": 3}
BaseJSON.remove_key({"a": 1, "b": 2}, "b") # {"a": 1}
BaseJSON.remove_keys({"a": 1, "b": 2, "c": 3}, ["b", "c"]) # {"a": 1}
Sort Keys Recursively
BaseJSON.sort_keys_deep({"b": 2, "a": {"d": 4, "c": 3}})
# {"a": {"c": 3, "d": 4}, "b": 2}
Find Values
d = {"a": 1, "b": {"c": "hello", "d": 42}}
BaseJSON.find_values(d, lambda v: isinstance(v, int))
# {"a": 1, "b.d": 42}
Structural Subset Comparison
json1 = {"a": 1, "b": {"c": 2, "d": [1, 2, 3]}}
json2 = {"b": {"d": [2]}}
assert BaseJSON.are_json_equal(json1, json2) # True (json2 is a subset of json1)
Serialization Helpers
s = BaseJSON.to_string({"ok": True}) # '{"ok": true}'
obj = BaseJSON.from_string('{"ok": true}') # {"ok": True}
# Validity check
BaseJSON.is_valid_json('{"a": 1}') # True
BaseJSON.is_valid_json('{bad}') # False
# Pretty-print
print(BaseJSON.pretty_print({"b": 2, "a": 1}, sort_keys=True))
Introspection
d = {"a": 1, "b": {"c": 2, "d": 3}}
BaseJSON.list_keys(d) # ["a", "b"]
BaseJSON.list_keys(d, nested=True) # ["a", "b.c", "b.d"]
BaseJSON.count_keys(d) # 3 (leaf keys)
BaseJSON.count_keys(d, nested=False) # 2 (top-level)
Matrix Helpers
matrix = [[1, 2], [3, 4]]
BaseJSON.save_matrix_to_json(matrix, "./tmp/matrix.json")
loaded = BaseJSON.load_matrix_from_json("./tmp/matrix.json")
assert loaded == matrix
Exception Hierarchy
JSONError
+-- JSONFileNotFoundError
+-- JSONDecodeError
+-- JSONSerializationError
+-- JSONKeyError
+-- JSONPathError
+-- JSONMergeError
+-- JSONValidationError
All exceptions inherit from JSONError for convenient broad catches.
API Reference
BaseJSON - I/O
| Method | Description |
|---|---|
load_json(path) |
Read and parse a JSON file |
load_or_default(path, default=None) |
Load JSON or return default if file missing |
write_json(path, data, *, indent=4, ensure_ascii=False) |
Write data to a JSON file (creates parents) |
BaseJSON - Key / Value
| Method | Description |
|---|---|
add_value_to_json(file_path, attribute, value) |
Add or update a top-level key in a JSON file |
extract_value_from_json(file_path, attribute) |
Get the value of a top-level key |
check_value_in_json(file_path, value) |
Check if a value exists among JSON values |
remove_key(data, key) |
Return a copy without key |
remove_keys(data, keys) |
Return a copy without the specified keys |
BaseJSON - Nested Access
| Method | Description |
|---|---|
get_nested(data, path, default=SENTINEL, *, separator=".") |
Get value via dot-notation |
set_nested(data, path, value, *, separator=".") |
Set value via dot-notation (creates intermediates) |
has_nested(data, path, *, separator=".") |
Check if a dot-notation path exists |
delete_nested(data, path, *, separator=".") |
Delete a key at a dot-notation path |
BaseJSON - Transform
| Method | Description |
|---|---|
deep_merge(base, override) |
Recursively merge two dicts (override wins) |
flatten(data, *, separator=".") |
Flatten nested dict to dotted keys |
unflatten(data, *, separator=".") |
Reconstruct nested dict from flat keys |
filter_keys(data, keys) |
Keep only specified keys |
exclude_keys(data, keys) |
Remove specified keys |
are_json_equal(json1, json2) |
Structural subset comparison |
list_keys(data, *, nested=False, separator=".") |
List keys (top-level or all leaf paths) |
count_keys(data, *, nested=True) |
Count keys |
diff(original, modified) |
Compute added/removed/changed keys |
sort_keys_deep(data) |
Recursively sort all dict keys |
find_values(data, predicate) |
Find leaf values matching a predicate |
BaseJSON - Serialization
| Method | Description |
|---|---|
to_string(data, *, indent=None, ensure_ascii=False, sort_keys=False) |
Serialize to JSON string |
from_string(text) |
Parse a JSON string |
is_valid_json(text) |
Check if string is valid JSON |
pretty_print(data, *, indent=2, sort_keys=False) |
Human-readable formatted JSON |
save_matrix_to_json(matrix, file_path) |
Save 2-D matrix to JSON file |
load_matrix_from_json(file_path) |
Load 2-D matrix from JSON file |
Type Aliases
| Name | Definition |
|---|---|
JSONType |
Union[Dict[str, Any], List[Any], str, int, float, bool, None] |
PathLike |
Union[str, Path] |
Development
pip install -e ".[dev]"
# Run tests
python -m pytest tests/ -v
# Lint
ruff check .
# Type check
mypy ouroboros_json/
License
This project is licensed under the MIT License - see the LICENSE file for details.
Authors
Flavio Brandolini
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ouroboros_json-1.0.0.tar.gz.
File metadata
- Download URL: ouroboros_json-1.0.0.tar.gz
- Upload date:
- Size: 21.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4dd186c876f4f364f5888e8867233fbf3205cdfa75db5567027e4d99488aec2e
|
|
| MD5 |
5c2160494c193b0f116f1036fd6e0bef
|
|
| BLAKE2b-256 |
53704104c8b8bfe3e53f0aca039de0934737eea761a28f59d0e534fc8c3b52e6
|
File details
Details for the file ouroboros_json-1.0.0-py3-none-any.whl.
File metadata
- Download URL: ouroboros_json-1.0.0-py3-none-any.whl
- Upload date:
- Size: 18.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0d301e37d5f2c59515d532a53254ac69ffab12675e315d112b9be75d439db12a
|
|
| MD5 |
506441b462ee11081b9acb36a46e70c1
|
|
| BLAKE2b-256 |
f4a6adf2a3f62ac2835a699df753bd7a48e839dd2a73def7726a348bdc509a3a
|