Batu Lab NotesPractical developer guides

Preserve list order when it has domain meaning

By Batu · English technical notes

Also published in our primary archive.

To preserve list order when it has domain meaning in Python, do not run a whole JSON document through a recursive canonicalizer that sorts every list. Keep JSON arrays in their incoming order; if you need deterministic JSON text, restrict canonicalization to object-key ordering with sort_keys=True.

This fixture shows a concrete boundary: a request normalizer receives JSON for a workflow. Its faulty canonicalize_everything() function sorts every list before emitting the normalized request. That changes the steps array from validate, write, verify to validate, verify, write; the simulated executor therefore records a different execution order. This is not a decoder failure—json.loads() first produces the correct Python list. The failure happens at the normalizing interface, where a broad determinism rule is applied to data whose order is contractual.

The repaired path serializes the decoded request with json.dumps(..., sort_keys=True). That makes object-key order stable in the JSON text while leaving list elements untouched. The assertions check both properties: the corrected JSON has its top-level keys alphabetized, and the decoded steps value exactly matches the original three-step sequence. They do not prove that another service will treat these names as workflow instructions; that remains the API contract.

The Python json documentation maps JSON arrays to Python lists and documents sort_keys; the list documentation describes lists as ordered collections. These APIs are long-standing; Python 3.8+ is a practical minimum for this example.

AI assistance disclosure: this article was drafted with AI assistance and uses a synthetic in-memory request fixture.

import json


request_text = (
    '{"workflow":"release","steps":["validate","write","verify"],'
    '"metadata":{"run_id":7,"team":"docs"}}'
)
request = json.loads(request_text)


def canonicalize_everything(value):
    if isinstance(value, dict):
        return {key: canonicalize_everything(value[key]) for key in sorted(value)}
    if isinstance(value, list):
        return sorted(canonicalize_everything(item) for item in value)
    return value


def execute(workflow_request):
    return " -> ".join(workflow_request["steps"])


wrong_request = canonicalize_everything(request)
wrong_execution = execute(wrong_request)
assert wrong_request["steps"] == ["validate", "verify", "write"]
assert wrong_execution == "validate -> verify -> write"

corrected_text = json.dumps(request, sort_keys=True, separators=(",", ":"))
corrected_request = json.loads(corrected_text)
corrected_execution = execute(corrected_request)
assert corrected_text.startswith('{"metadata":')
assert corrected_request["steps"] == ["validate", "write", "verify"]
assert corrected_execution == "validate -> write -> verify"

print(wrong_execution)
print(corrected_text)
print(corrected_execution)
validate -> verify -> write
{"metadata":{"run_id":7,"team":"docs"},"steps":["validate","write","verify"],"workflow":"release"}
validate -> write -> verify