Batu Lab NotesPractical developer guides

Validate optional JSON-like fields at the boundary

By Batu ยท English technical notes

Also published in our Blogger archive.

Validate optional JSON-like fields at the boundary

json.loads decodes JSON into ordinary Python values, so its result should be treated as untrusted shape data at an application boundary. This function first requires a JSON object, then validates two optional fields. An absent theme becomes "system"; when present it must be a string. An absent retries becomes 3; when present it must be a non-negative integer, with booleans excluded explicitly because Python considers them integers for isinstance purposes.

The synthetic JSON input contains only theme, so the normalized output demonstrates both an accepted supplied field and the defaulted optional field. Assertions exercise rejection of a list in place of an object and a negative retry count. These checks deliberately cover only this small schema: they do not reject unknown keys, validate a larger configuration format, or turn JSON decoding into complete security validation. Decide those policies at the same boundary when the application needs them. This example uses built-in generic annotations such as dict[str, object], which require Python 3.9 or later.

See the official json.loads documentation. AI assistance disclosure: this article was drafted with AI assistance and checked using a synthetic example.

import json


def validate_settings(value: object) -> dict[str, object]:
    if not isinstance(value, dict):
        raise ValueError("settings must be an object")

    theme = value.get("theme", "system")
    retries = value.get("retries", 3)
    if not isinstance(theme, str):
        raise ValueError("theme must be a string")
    if isinstance(retries, bool) or not isinstance(retries, int) or retries < 0:
        raise ValueError("retries must be a non-negative integer")
    return {"theme": theme, "retries": retries}


settings = validate_settings(json.loads('{"theme": "dark"}'))
assert settings == {"theme": "dark", "retries": 3}
for invalid in ([], {"retries": -1}):
    try:
        validate_settings(invalid)
    except ValueError:
        pass
    else:
        raise AssertionError("invalid settings were accepted")
print(f"theme={settings['theme']} retries={settings['retries']}")
theme=dark retries=3