Python can successfully parse a JSON configuration file that repeats the same key. By default, the later value wins. If a deployment file contains two different environment names, that behaviour can conceal a mistake instead of stopping the job. Use object_pairs_hook to reject repeated keys while the decoder still has the original name/value pairs, then validate the resulting configuration separately.
Why a dictionary check comes too late
Consider {"environment": "test", "environment": "production"}. Once ordinary decoding has produced a dictionary, the earlier value is gone. Counting dictionary keys afterward cannot recover that evidence. The hook runs during decoding and receives an ordered list of pairs for each JSON object, including objects nested inside arrays or other objects.
The official Python JSON documentation describes this hook and notes that the default decoder also accepts NaN and Infinity, which are outside the JSON specification. Both behaviours matter when a configuration file is expected to be unambiguous and portable.
Build a small configuration parser
This Python 3.12 example uses only the standard library. It reads at most one byte beyond an example size limit, rejects repeated object names and disallows the special numeric constants. The size limit is a local policy choice, not a universal safe limit for every application.
import json
from pathlib import Path
MAX_BYTES = 100_000
def unique_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError(f'Duplicate JSON key: {key!r}')
result[key] = value
return result
def reject_constant(value):
raise ValueError(f'Unsupported JSON number: {value}')
def parse_config(text):
data = json.loads(
text,
object_pairs_hook=unique_object,
parse_constant=reject_constant,
)
if not isinstance(data, dict):
raise ValueError('Configuration must be an object')
return data
def load_config(path):
with Path(path).open('rb') as stream:
raw = stream.read(MAX_BYTES + 1)
if len(raw) > MAX_BYTES:
raise ValueError('Configuration exceeds the size limit')
return parse_config(raw.decode('utf-8'))
The UTF-8 decoding is deliberate: it establishes an explicit file-format expectation for this application. Keep the original file unchanged when validation fails. At the application boundary, handle OSError, UnicodeDecodeError and ValueError, report a useful error, and exit before performing deployment actions. JSONDecodeError is a ValueError subclass; it also exposes line and column information for syntax failures.
Test syntax, duplicates and nesting separately
Save the functions in a disposable project and run the following focused checks. These are expected behaviours to verify in your environment, not a claim about an existing production configuration.
valid = parse_config('{"environment": "test"}')
assert valid['environment'] == 'test'
bad_inputs = [
'{"environment": "test", "environment": "production"}',
'{"service": {"port": 8000, "port": 9000}}',
'{"timeout": NaN}',
'{"environment": }',
'[]',
]
for sample in bad_inputs:
try:
parse_config(sample)
except ValueError:
pass
else:
raise AssertionError('Invalid configuration was accepted')
Repeating a name in two different objects remains legitimate. For example, two service objects can each have a port field. This parser rejects duplicates within one object, not every repeated word anywhere in the document. Add a successful nested-object test so a future refactor does not accidentally impose that broader restriction.
Validate meaning after decoding
Unique keys do not make a configuration correct. Next check required fields, permitted values and exact types. For a port, type(port) is int avoids accepting a boolean, because Python booleans are integer subclasses. Then check the permitted range and your application’s deployment policy. Reject unknown fields if silently ignoring a misspelled option would be dangerous.
Also distinguish numeric constants from numeric range: rejecting NaN and Infinity tokens does not by itself guarantee that every decoded float is finite. An extreme exponent can overflow to infinity. Where finite floats are required, apply an explicit finite-number check during semantic validation or choose a suitable decimal parsing strategy.
This separation mirrors validating CSV headers before an inventory import: verify the input contract before acting on its values.
Pre-deployment checklist and FAQ
- Keep a known-good configuration and a rollback procedure.
- Test duplicate keys at the top level and inside nested objects.
- Reject oversize files before fully loading them into memory.
- Validate field names, types, ranges and allowed environments.
- Log the failure location without dumping secrets.
Does strict=True reject duplicate keys? No. That decoder option controls unescaped control characters inside strings, not repeated object names. Does this replace a schema? No. It addresses parsing ambiguity; application-specific validation remains necessary.
Safety notice: This tutorial is for educational purposes. Test carefully with disposable files before production use. The site owner is not responsible for misuse, damage, data loss or production issues.


