Sembra
Structural similarity scoring for JSON-compatible data
Italian: "sembrare" — to seem, to resemble
The Problem
Existing deep comparison libraries answer only binary questions:
| Library | Gives you |
|---|---|
fast-deep-equal |
Equal or not? |
lodash.isequal |
Equal or not? |
deep-diff |
What changed? |
deepdiff (Python) |
What changed? |
None answer the most natural question:
How similar are these two structures?
When schemas evolve, keys get renamed, or data comes from different sources, binary equality is useless. You need a similarity score.
The Algorithm: Sembra Similarity
Sembra computes a continuous similarity score S(a, b) ∈ [0, 1] for any two JSON-compatible values using a novel multi-phase algorithm:
Phase 1: Type-Aware Primitive Matching
- Strings: Jaro-Winkler distance (handles typos, minor variations)
- Numbers: Relative difference scoring
- Booleans: Exact match (configurable partial mode)
- Null: Always exact
Phase 2: Optimal Array Alignment
Instead of index-by-index comparison, Sembra computes a similarity matrix between all pairs of elements and finds the optimal matching using a greedy assignment algorithm. This handles:
- Reordered elements
- Inserted/deleted elements
- Partially matching elements
Phase 3: Value-Guided Key Rename Detection
This is the key novelty. When object keys differ between two structures, Sembra infers renames by comparing the values at those keys:
a = {"name": "Alice", "age": 30}
b = {"fullName": "Alice", "yearsOld": 30}
# Sembra detects: name → fullName, age → yearsOld
# Score: >0.85
No other library does this.
Phase 4: Recursive Composition
All phases compose recursively — nested arrays, nested objects, and mixed structures are handled uniformly.
Performance
| Operation | Complexity |
|---|---|
| Identical structures (early exit) | O(n) |
| Similar structures | O(n log n) |
| Dissimilar structures | O(n × m) |
n = number of nodes in the larger structure.
Quick Start
from sembra import sembra, sembra_report, SembraConfig
# Basic similarity
score = sembra({"a": 1, "b": 2}, {"a": 1, "b": 3})
print(score) # ~0.89
# Key rename detection
score = sembra(
{"userName": "Alice", "userAge": 30},
{"name": "Alice", "age": 30}
)
print(score) # >0.8
# Reordered arrays
score = sembra([1, 2, 3], [3, 1, 2])
print(score) # 1.0 (all elements match)
# Detailed report
report = sembra_report({"a": 1}, {"a": 2})
print(report.score)
print(report.matches)
print(f"Computed in {report.duration_ns}ns")
# Configuration
config = SembraConfig(
string_similarity=False,
object_key_rename=False,
number_threshold=0.9,
)
score = sembra({"a": "hello"}, {"a": "hallo"}, config)
Configuration Reference
| Option | Default | Description |
|---|---|---|
string_similarity |
True |
Enable fuzzy string matching |
string_threshold |
0.6 |
Minimum string similarity to count as match |
number_similarity |
True |
Enable fuzzy number matching |
number_threshold |
0.8 |
Minimum number similarity to count as match |
boolean_partial |
False |
Allow partial boolean matching |
array_matching |
True |
Enable optimal array element matching |
array_match_threshold |
0.4 |
Minimum element similarity to match |
object_key_rename |
True |
Enable value-guided key rename detection |
object_rename_threshold |
0.65 |
Minimum value similarity to infer rename |
max_depth |
16 |
Maximum recursion depth |
max_array_size |
200 |
Max array size for element-by-element matching |
use_cache |
True |
Cache intermediate results |
early_exit |
True |
Exit early for same-reference objects |
key_weight_mode |
'uniform' |
Key importance weighting |
Use Cases
- Test assertions with tolerance for structural variation
- Schema migration — map old API responses to new schemas
- Data deduplication — find structurally similar records
- Configuration comparison — detect drifted configs
- API monitoring — alert on structural drift
- Merge conflict resolution — find best correspondence
Comparison with Existing Libraries
| Feature | Sembra | deepdiff | fast-deep-equal | deep-diff |
|---|---|---|---|---|
| Continuous similarity score | ✅ | ❌ | ❌ | ❌ |
| Key rename detection | ✅ | ❌ | ❌ | ❌ |
| Array alignment | ✅ | ❌ | ❌ | ❌ |
| Fuzzy strings | ✅ | ❌ | ❌ | ❌ |
| Fuzzy numbers | ✅ | ❌ | ❌ | ❌ |
| Binary equality | ✅ | ✅ | ✅ | ❌ |
| Diff script | ✅ | ✅ | ❌ | ✅ |
| Cross-type matching | ✅ | ❌ | ❌ | ❌ |
License
MIT — see LICENSE
Author
nhemlos
Metadata
Release files for sembra 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sembra-0.1.0.tar.gz | 75.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sembra-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 83.4 kB
Release files / sembra-0.1.0.tar.gz
| Download URL | sembra-0.1.0.tar.gz |
|---|---|
| Size | 75.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e591e20bee44b732deecab9d9a73557feb0afc5ff5bcacf5278a8b51b2cfbe02
|
|
BLAKE2b-256 checksum How to use checksums |
234089fbe31754c009e04f06b65e374b226f21d516208fb3d5bc1d102a741522
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.9
|
Release files / sembra-0.1.0-py3-none-any.whl
| Download URL | sembra-0.1.0-py3-none-any.whl |
|---|---|
| Size | 8.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4360d798dedeb8e8e454e55b6e38c7f859b6c7b0f728f424b925417cd8d04335
|
|
BLAKE2b-256 checksum How to use checksums |
78c6f1d86458712ace2c125583aac0a1b1937efa540925e636caaa8088884c3a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.9
|