One line of the specification decides this whole exercise. Read which of the two collections gets sorted.
import json
a = json.loads('{"x": 1, "y": 2}')
b = json.loads('{"y": 2, "x": 1}')
print(a == b) # -> True objects are unordered
c = json.loads("[1, 2]")
d = json.loads("[2, 1]")
print(c == d) # -> False arrays are ordered
print(json.dumps({'b': 1, 'a': 2}, sort_keys=True, indent=2))
- Objects are unordered; arrays are not
Two JSON objects with the same pairs are the same value however they are written, so sorting the keys loses nothing and makes them comparable. An array’s order is part of its meaning — [1, 2] is not [2, 1] — so sorting one would change the data rather than tidy it. Getting this backwards is the only real way to break this function.
- sort_keys goes all the way down
json.dumps sorts keys at every level of nesting, not just the top, which is what makes the output genuinely canonical. You do not have to walk the structure yourself.
- ensure_ascii defaults to True, and it is rarely what you want
By default json.dumps turns 'café' into 'caf\\u00e9' — safe for ancient transports and unreadable in a diff. ensure_ascii=False keeps the characters, which matters the moment your data has names in it.
Two calls, and the whole exercise is in their keyword arguments. Parse the text into Python, then write it back out — the question is what to ask for on the way out.
json.loads parses; json.dumps writes. dumps takes sort_keys, indent and ensure_ascii, and the contract names exactly what each should be.
return json.dumps(json.loads(text), sort_keys=True, indent=2, ensure_ascii=False). There is no error handling to write: a decode failure already raises a ValueError subclass, which is what the contract asks for.
import json def normalise_json(text): return json.dumps( json.____(text), sort_keys=____, indent=____, ensure_ascii=____, )
import json
def normalise_json(text):
return json.dumps(
json.loads(text),
sort_keys=True,
indent=2,
ensure_ascii=False,
)
json.JSONDecodeError subclasses ValueError, so invalid input already raises exactly what the contract promises — with the line and column of the problem, which your own message would not have. Catching it to re-raise a plainer ValueError would be strictly worse. The grader accepts it for the same reason `except ValueError:` would catch it: a subclass *is* the thing it inherits from. Knowing an exception’s ancestry is what tells you whether to leave it alone.
This is the one judgement in the exercise. The JSON specification says object members are unordered and array elements are not, so sorting keys preserves meaning while sorting a list destroys it — and would make two genuinely different payloads compare equal. Normalising must never make different things look the same.
The point is that the *diff* becomes readable — one changed line instead of a reordered file. That is why indent matters at all: a single-line dump would be canonical too, and useless to a line-based diff tool. Design the output for whatever is going to consume it.
Do not use this to compare payloads programmatically. If the question is 'are these the same?', compare the parsed objects directly — json.loads(a) == json.loads(b) — rather than their rendered text, which is faster and unaffected by formatting choices. This function is for producing something a *person* or a line-based diff tool will read. Note too that it is not a canonical form in the cryptographic sense: JCS (RFC 8785) exists for when the bytes are being signed, and it has stricter rules about number formatting than json.dumps follows.
- Add an option to drop keys whose value is null, so a diff between an old API version and a new one does not light up on optional fields.
- Write a version that reports *which* paths differ between two payloads rather than producing text to diff — and notice it is deep-get walking two structures at once.
- Compare your output against RFC 8785 canonical JSON and list what would have to change if the result were going to be signed.