Validate Retry Counts: Decode JSON, Reject booleans, Check range

Reject Boolean Retry Counts in Python JSON Configuration

Validate JSON retry counts without accepting true as 1: distinguish type checks, range checks, missing values and deliberate configuration policy.

A Python check such as isinstance(value, int) also accepts True and False. That can silently turn a JSON configuration mistake into a retry policy: JSON true becomes Python True, which behaves like one in many numeric contexts. If your configuration requires a whole-number retry count rather than a flag, validate that contract explicitly before starting work.

This tutorial uses an example application that allows zero through five additional attempts after the initial attempt. That range is a teaching policy, not a Python limit or a recommendation for every service. The code validates configuration only; it does not send requests, perform retries, or change files.

Understand why the obvious check accepts a flag

The official Python documentation describes bool as a subclass of int. Python’s default JSON decoder maps integer numbers to int and the literals true and false to the Boolean instances. Successfully decoding a JSON document therefore establishes syntax, not compliance with your application’s field rules.

For example, a condition that combines an integer check with a zero-to-five range still accepts both Boolean values. Do not solve this by immediately calling int(value): conversion accepts inputs you may want to reject, including numeric strings and booleans, and truncates some floating-point values. Decide whether conversion is part of the contract before introducing it.

Validate the object, field type and allowed range

The following self-contained Python 3 example deliberately requires an ordinary built-in integer. That fits values produced by the default JSON decoder and excludes booleans. Save it as a disposable script and run it with your Python 3 interpreter.

import json


def read_retry_count(document):
    config = json.loads(document)
    if not isinstance(config, dict):
        raise ValueError('configuration must be a JSON object')
    if 'retries' not in config:
        raise ValueError('retries is required')
    value = config['retries']
    if type(value) is not int:
        raise ValueError('retries must be an integer, not a flag or string')
    if not 0 <= value <= 5:
        raise ValueError('retries must be between 0 and 5')
    return value


assert read_retry_count('{"retries": 0}') == 0
assert read_retry_count('{"retries": 3}') == 3
assert read_retry_count('{"retries": 5}') == 5

invalid = [
    '{"retries": true}',
    '{"retries": false}',
    '{"retries": 3.0}',
    '{"retries": "3"}',
    '{"retries": null}',
    '{"retries": -1}',
    '{"retries": 6}',
    '{}',
    '[]',
]
for document in invalid:
    try:
        read_retry_count(document)
    except ValueError:
        pass
    else:
        raise AssertionError('unexpectedly accepted: ' + document)

print('retry-count validation checks passed')

Keep the function definition separate from the tests when integrating it. The assertions are a small behaviour fixture, not production input validation: Python can disable assertions in optimised execution. The function itself uses explicit exceptions, so its checks do not depend on assertions being enabled.

Keep zero, missing and null distinct

Zero is a valid value in this example: it means no additional attempts. A truthiness check such as if not value would mix zero with missing-like or false values and reject a legitimate configuration. The explicit membership check instead gives a missing field its own error.

JSON null is different again. Here it is rejected rather than interpreted as a default. Another application might document null as automatic behaviour, but that would require a deliberate separate branch. Do not infer the same meaning for every API or configuration format. For related import safeguards, see rejecting duplicate JSON keys before loading configuration; duplicate-key handling is a separate concern from validating the final field value.

Choose strictness that matches your input boundary

Using type(value) is int rejects every subclass, not just bool. That is appropriate for this ordinary JSON boundary. A reusable function that intentionally accepts other integer subclasses could instead combine isinstance(value, int) with an explicit rejection of isinstance(value, bool). Custom decoder hooks can also change the objects produced, so revisit the contract if you change decoding behaviour.

Neither approach proves that the retry mechanism is safe. Separately define timeouts, delays, an overall deadline and which operations can be repeated without duplicate side effects. A valid number does not authorise repeating an ambiguous payment or publish request. This example also does not reject unrelated configuration keys or implement a complete schema.

Checklist before integration

  • Document whether retries means additional attempts or total attempts.
  • Choose and explain the range for the actual application.
  • Test true, false, zero, both range boundaries and values outside them.
  • Test a float, a numeric string, null, a missing key and a non-object root.
  • Handle decoding and validation errors before any external operation starts.

Why reject 3.0? The default decoder produces a float, and this contract requires an integer representation. Does parsing valid JSON guarantee valid configuration? No. Parsing and application validation answer different questions.

Sources and educational-use notice

Sources: Python built-in types and Boolean inheritance and the JSON decoder’s conversion table. This tutorial is for educational purposes. Test carefully before production use. The site owner is not responsible for misuse, damage, data loss, or production issues.

Leave a Reply

Your email address will not be published. Required fields are marked *