Technical note · python / api / test
Comparing API responses with Python: missing fields, null, and type differences
A fully offline comparison example that preserves field presence and type differences in small JSON responses.
When comparing two API responses, looking only at the result of before == after can hide some details. A field being absent, being present with null, and changing its value's type are different events. The purpose of a small comparison helper is to keep these distinctions visible during review. The example here sends no network requests; it works with two small, predefined dictionaries.
Track field presence separately
In Python, JSON null is represented by None. A dictionary's get() call also returns None for a missing key by default. Comparing only the results of get() therefore conflates a missing field with a field explicitly set to null. The solution is to carry present information alongside each value. Python json documentation, Python dictionary operations
before = {}
after = {"owner": None}
left = {"present": "owner" in before, "value": before.get("owner")}
right = {"present": "owner" in after, "value": after.get("owner")}
# left: {"present": False, "value": None}
# right: {"present": True, "value": None}Using the union of the keys matters too. Iterating only over fields in the earlier response misses fields added later. The package's semantic_diff iterates over fields from both sides and reports differences with their before and after values.
Do not confuse booleans with numbers
In Python equality comparisons, False == 0 and True == 1 are true; bool is a subclass of int. In an API contract, however, a flag and a number can have different meanings. In the example, records are compared after being serialized with json.dumps(..., sort_keys=True, allow_nan=False). This keeps the boolean/number distinction visible even in nested lists. Dictionary key order does not affect the result; list order does. Python bool explanation, json conversion options
This method is not a general definition of semantic equality. For example, 1 and 1.0 produce different JSON representations and count as a difference. This is a deliberate comparison choice in the example, not a mandatory rule for every API contract. The location of nested changes is not reported at the leaf level either; the entire relevant top-level field is reported.
Which fields should be excluded?
In this sample data, only the top-level request_id is excluded, on the assumption that it is a technical correlation field. id, owner, and tenant are retained. Before ignoring a field in your own response, establish the reason; the same name does not mean the same thing in every application. A request_id inside a nested object is not excluded in this example.
The runnable version accepts only dictionaries with string keys, lists, and finite JSON values. Non-string keys, non-JSON Python types such as tuples, and NaN/infinity are rejected. For these small examples, there are also limits of 32 levels of depth and 10,000 nodes per input. These are not general-purpose guarantees for handling large, malicious inputs.
Try it offline
You can get the example and tests by downloading the code package. After extracting the ZIP file, use the python-example folder. No dependencies beyond the standard library are required:
cd python-example
python3 semantic_diff.py
python3 -m unittest -vThe tests cover missing/null, boolean/number, nested value, list order, excluded field, and invalid input behavior. The unittest documentation explains how to run the tests. The output is a review aid; on its own, it is not evidence of BOLA or any other vulnerability. Ownership, sharing policy, and user context must be evaluated separately.