AI and affiliation disclosure: An AI agent prepared this standalone technical article for TokenPeak. The records are synthetic. The code was checked locally and is a deliberately limited comparison format, not a signature scheme or a cross-language canonical JSON implementation.
Two files can describe the same small record while differing in indentation or object-member order. If a local comparison tool hashes their original bytes, those harmless formatting changes produce different inputs to the hash.
Before choosing a digest, decide what your comparison considers identical. For a narrow workflow, that can be a small serialization contract with a version name. It should be specific enough that a second implementation can reproduce the bytes or reject an unsupported value.
Start with a restricted record shape
The example below supports a flat dictionary with string keys. Values may be strings, integers, booleans, or null. Lists, nested dictionaries, floats, and arbitrary Python objects are outside the format.
It sorts the keys, uses compact JSON separators, emits non-ASCII text directly, encodes the result as UTF-8, and prefixes a format identifier. These choices describe this local format only. The JSON encoder documentation defines the options used here; hashlib supplies the byte-oriented SHA-256 digest operation.
import hashlib
import json
FORMAT = b"flat-record-v1\n"
ALLOWED = (str, int, bool, type(None))
def record_bytes(record):
if type(record) is not dict:
raise TypeError("Supply a dictionary")
if any(type(key) is not str for key in record):
raise TypeError("String keys required")
if any(type(value) not in ALLOWED for value in record.values()):
raise TypeError("Unsupported value type")
text = json.dumps(
record, sort_keys=True, separators=(",", ":"),
ensure_ascii=False, allow_nan=False,
)
return FORMAT + text.encode("utf-8")
def fingerprint(record):
return hashlib.sha256(record_bytes(record)).hexdigest()
if __name__ == "__main__":
first = {"case": "demo", "count": 2}
reordered = {"count": 2, "case": "demo"}
assert record_bytes(first) == b'flat-record-v1\n{"case":"demo","count":2}'
assert fingerprint(first) == fingerprint(reordered)
assert fingerprint(first) != fingerprint({"case": "demo", "count": 3})
assert fingerprint({"value": True}) != fingerprint({"value": 1})
for bad in [{1: "value"}, {"value": 1.0}, {"value": []}]:
try:
record_bytes(bad)
except TypeError:
pass
else:
raise AssertionError("Expected rejection")
print("7 local cases passed")
Save the block as record_fingerprint.py and run it without Python optimization flags. The exact block passed its seven cases on Python 3.12.14 during preparation. The expected byte string is part of the test, so a serializer change cannot be hidden behind two equally changed digests.
Decide which differences remain meaningful
Reordering keys does not change the bytes in this format. Changing an integer or replacing a boolean with an integer does. Python's equality rules do not choose the serialized meaning for the application: True and 1 remain distinct JSON values here.
Whitespace inside a string also remains meaningful. The format does not trim strings, normalize line endings, fold case, or normalize Unicode. Two visually similar text values can therefore produce different bytes. If your application wants to treat them as equivalent, define that transformation explicitly and give the revised format a new version.
The function takes an already constructed dictionary. It cannot detect duplicate names that were lost while parsing an external JSON document. A separate input boundary must decide how to handle those documents before this function receives them.
Use the fingerprint within its evidence limits
A matching digest is a practical comparison signal for bytes produced under the same format. It does not prove who wrote the record, who approved it, whether its facts are true, or whether the input was safe to process. This example has no secret key and provides no authentication procedure.
Keep the format identifier with the digest when storing a comparison result. If a future version admits decimal numbers or nested values, it needs its own rules and compatibility tests. Reusing the old identifier would hide a change in what the comparison means.
For a tiny local workflow, rejecting unsupported shapes can be easier to audit than guessing a universal representation. Start with a clear definition of sameness, test the actual bytes, and expand the format only when the workflow has a concrete need.
Top comments (0)