Diff JSON and JSON-like structures in Python with multiple syntax support and bidirectional patching
—
The main class providing configurable diff computation with options for syntax, serialization, marshaling, and path exclusion. JsonDiffer serves as the central orchestrator for all diff operations and provides fine-grained control over the diff process.
Central class managing diff computation with comprehensive configuration options for different use cases and output requirements.
class JsonDiffer:
"""
A class for computing differences between two JSON structures and applying patches.
"""
def __init__(self, syntax='compact', load=False, dump=False, marshal=False,
loader=None, dumper=None, escape_str='$'):
"""
Initialize JsonDiffer with specified options.
Parameters:
- syntax: str or syntax class, diff output format (default: 'compact')
- 'compact': Minimal output size
- 'explicit': Human-readable with clear operation labels
- 'symmetric': Bidirectional with original and modified values
- 'rightonly': Focus on final state values
- load: bool, automatically load JSON from strings or files (default: False)
- dump: bool, automatically dump output to JSON strings or files (default: False)
- marshal: bool, marshal diffs to handle special characters (default: False)
- loader: callable, custom function for loading JSON data (default: built-in JsonLoader)
- dumper: callable, custom function for dumping JSON data (default: built-in JsonDumper)
- escape_str: str, string used to escape special characters (default: '$')
"""
def diff(self, a, b, fp=None, exclude_paths=None):
"""
Compute difference between two JSON structures.
Parameters:
- a: Original JSON structure
- b: Modified JSON structure
- fp: Optional file pointer to dump diff to
- exclude_paths: list of string paths to exclude from diff
Returns:
dict: Computed diff structure
"""
def similarity(self, a, b):
"""
Calculate similarity score between two JSON structures.
Parameters:
- a: First JSON structure
- b: Second JSON structure
Returns:
float: Similarity score between 0.0 and 1.0
"""
def patch(self, a, d, fp=None):
"""
Apply diff to JSON structure to produce modified structure.
Parameters:
- a: Original JSON structure to patch
- d: Diff to apply
- fp: Optional file pointer to dump result to
Returns:
JSON structure: Patched structure
"""
def unpatch(self, b, d, fp=None):
"""
Reverse diff on JSON structure to produce original structure.
Available only with symmetric syntax.
Parameters:
- b: Modified JSON structure
- d: Diff that was applied
- fp: Optional file pointer to dump result to
Returns:
JSON structure: Original structure before diff
"""
def marshal(self, d):
"""
Convert structure to marshaled form with escaped special characters.
Parameters:
- d: Structure to marshal
Returns:
Marshaled structure with escaped symbols
"""
def unmarshal(self, d):
"""
Convert marshaled structure back to original form.
Parameters:
- d: Marshaled structure to unmarshal
Returns:
Original structure with unescaped symbols
"""The JsonDiffer class provides extensive configuration through its Options inner class and constructor parameters.
class JsonDiffer:
class Options:
"""Configuration options container for JsonDiffer."""
passUsage Examples:
from jsondiff import JsonDiffer, JsonLoader, JsonDumper
# Basic configuration with different syntaxes
compact_differ = JsonDiffer(syntax='compact')
explicit_differ = JsonDiffer(syntax='explicit')
symmetric_differ = JsonDiffer(syntax='symmetric')
# Auto-loading from JSON strings
auto_loader = JsonDiffer(load=True)
result = auto_loader.diff('{"a": 1}', '{"a": 2, "b": 3}')
# Auto-dumping to JSON strings
auto_dumper = JsonDiffer(dump=True)
json_result = auto_dumper.diff({'a': 1}, {'a': 2}) # Returns JSON string
# Marshaling for safe serialization
marshal_differ = JsonDiffer(marshal=True)
result = marshal_differ.diff({'$delete': 'value'}, {'$delete': 'new_value'})
# Symbols are escaped: {'$$delete': 'new_value'}
# Custom loaders and dumpers
custom_loader = JsonLoader(parse_float=float, parse_int=int)
custom_dumper = JsonDumper(indent=2, sort_keys=True)
custom_differ = JsonDiffer(loader=custom_loader, dumper=custom_dumper)
# Custom escape string
custom_escape = JsonDiffer(escape_str='#')
# Symbols will be escaped with '#' instead of '$'Exclude specific JSON paths from diff computation, useful for ignoring timestamps, IDs, or other dynamic fields.
Usage Examples:
from jsondiff import JsonDiffer
differ = JsonDiffer()
# Single path exclusion
data1 = {'user': {'name': 'John', 'last_login': '2023-01-01', 'id': 123}}
data2 = {'user': {'name': 'John', 'last_login': '2023-01-02', 'id': 123}}
result = differ.diff(data1, data2, exclude_paths=['user.last_login'])
# Result: {} (no differences after excluding timestamp)
# Multiple path exclusion
data1 = {
'users': [
{'name': 'Alice', 'id': 1, 'created': '2023-01-01'},
{'name': 'Bob', 'id': 2, 'created': '2023-01-02'}
],
'metadata': {'version': '1.0', 'timestamp': '2023-01-01T10:00:00'}
}
data2 = {
'users': [
{'name': 'Alice', 'id': 1, 'created': '2023-01-01'},
{'name': 'Bob', 'id': 2, 'created': '2023-01-02'},
{'name': 'Charlie', 'id': 3, 'created': '2023-01-03'}
],
'metadata': {'version': '1.1', 'timestamp': '2023-01-03T15:30:00'}
}
result = differ.diff(data1, data2, exclude_paths=[
'metadata.timestamp',
'users.created' # Note: This excludes the path pattern, not array elements
])
# Result focuses on structural changes, ignoring timestampsConfigure JsonDiffer for specialized use cases with custom serialization and processing options.
Usage Examples:
from jsondiff import JsonDiffer, YamlLoader, YamlDumper
import json
# YAML processing configuration
yaml_differ = JsonDiffer(
load=True,
dump=True,
loader=YamlLoader(),
dumper=YamlDumper(default_flow_style=False)
)
# File-based operations
with open('config1.yaml', 'r') as f1, open('config2.yaml', 'r') as f2:
with open('diff.yaml', 'w') as output:
yaml_differ.diff(f1, f2, fp=output)
# High-precision numeric handling
class HighPrecisionLoader:
def __call__(self, src):
return json.loads(src, parse_float=lambda x: x) # Keep as string
precision_differ = JsonDiffer(loader=HighPrecisionLoader())
# Streaming large diffs
class StreamingDumper:
def __call__(self, obj, dest=None):
if dest:
json.dump(obj, dest, separators=(',', ':')) # Compact output
else:
return json.dumps(obj, separators=(',', ':'))
streaming_differ = JsonDiffer(dump=True, dumper=StreamingDumper())
# Combining multiple options
production_differ = JsonDiffer(
syntax='compact', # Minimal storage
marshal=True, # Safe for serialization
load=True, # Accept JSON strings
dump=True, # Return JSON strings
escape_str='@' # Custom escape character
)Handle special characters and symbols safely for serialization and storage.
Usage Examples:
from jsondiff import JsonDiffer, delete, insert
differ = JsonDiffer(marshal=True)
# Data with symbol-like keys
original = {'$delete': 'some_value', 'normal_key': 'data'}
modified = {'$delete': 'updated_value', 'normal_key': 'data', '$insert': 'new'}
# Marshal handles symbol conflicts
result = differ.diff(original, modified)
# Symbols are properly escaped to avoid conflicts
# Manual marshaling/unmarshaling
raw_diff = {delete: ['removed_key'], 'updated_key': 'new_value'}
marshaled = differ.marshal(raw_diff)
print(marshaled) # {'$delete': ['removed_key'], 'updated_key': 'new_value'}
unmarshaled = differ.unmarshal(marshaled)
print(unmarshaled) # {delete: ['removed_key'], 'updated_key': 'new_value'}
# Safe round-trip serialization
import json
# Create diff with symbols
diff_with_symbols = differ.diff({'a': 1}, {'b': 2})
marshaled_diff = differ.marshal(diff_with_symbols)
# Serialize safely
json_str = json.dumps(marshaled_diff)
loaded_diff = json.loads(json_str)
# Unmarshal and apply
final_diff = differ.unmarshal(loaded_diff)
result = differ.patch({'a': 1}, final_diff)JsonDiffer handles various error conditions and provides clear error messages:
from jsondiff import JsonDiffer
from json import JSONDecodeError
differ = JsonDiffer()
# Invalid syntax specification
try:
invalid_differ = JsonDiffer(syntax='nonexistent')
except (KeyError, AttributeError) as e:
print(f"Invalid syntax: {e}")
# Load errors with auto-loading
auto_differ = JsonDiffer(load=True)
try:
result = auto_differ.diff('{"invalid": json}', '{"valid": "json"}')
except JSONDecodeError as e:
print(f"JSON parsing error: {e}")
# Unpatch without symmetric syntax
compact_differ = JsonDiffer(syntax='compact')
try:
result = compact_differ.unpatch({'a': 2}, {'a': 1})
except AttributeError as e:
print(f"Unpatch not supported: {e}")Install with Tessl CLI
npx tessl i tessl/pypi-jsondiff