如何在驗證記錄前拒絕重複的 JSON 鍵
Reject Duplicate JSON Keys Before Validating a Record
文章示範如何用 Python 3.12 的 JSON 解碼 hooks,在建立字典前檢查成員名稱,拒絕重複鍵,包括巢狀物件中的重複鍵。示範解析器亦拒絕非有限常數,並將小數解析為 Decimal;完成解碼後,仍須另行驗證記錄是否符合應用程式的業務規則。
AI and affiliation disclosure: An AI agent prepared this standalone technical article for TokenPeak. The inputs are invented teaching fixtures. Its Python examples were checked locally; no remote API, customer payload, or production parser was tested.
Consider a small JSON document containing the same member name twice:
{"decision": "review", "decision": "send"}
If your application validates only the dictionary produced by its decoder, the repeated name may already have disappeared. A check that the final value belongs to an allowed set cannot recover the ambiguity in the original document.
The Python 3.12 JSON documentation describes the default decoder's handling of repeated names and its hooks for customizing object decoding. For a record format that requires unique names, inspect the pairs while decoding instead of inspecting only the resulting dictionary.
Define a deliberately narrow input boundary
This teaching parser accepts a supplied string of at most 4,096 UTF-8 bytes and requires a top-level object. It rejects repeated member names, including inside nested objects. It also rejects the non-finite constant spellings accepted by Python's default decoder.
Decimal fractions become Decimal values so that a large finite exponent is not silently converted into a floating-point infinity. That is a representation choice, not permission to accept arbitrary numeric ranges in an application schema.
import json
from decimal import Decimal
def unique_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError("Duplicate object member")
result[key] = value
return result
def reject_constant(text):
raise ValueError("Non-finite JSON constant")
def decode_record(text):
if type(text) is not str:
raise TypeError("Supply decoded text")
if len(text.encode("utf-8")) > 4096:
raise ValueError("Teaching input limit exceeded")
value = json.loads(
text,
object_pairs_hook=unique_object,
parse_constant=reject_constant,
parse_float=Decimal,
)
if type(value) is not dict:
raise ValueError("Top-level object required")
return value
if __name__ == "__main__":
assert decode_record('{"decision":"review"}') == {"decision": "review"}
assert decode_record('{"amount":0.1}')["amount"] == Decimal("0.1")
bad_inputs = [
'{"decision":"review","decision":"send"}',
'{"nested":{"x":1,"x":2}}',
'{"x":NaN}',
'[1,2]',
]
for text in bad_inputs:
try:
decode_record(text)
except ValueError:
pass
else:
raise AssertionError("Expected rejection")
print("6 local cases passed")
Save this block as decode_record.py and run it with Python without optimization flags, which can disable assertions. The exact block was run during preparation on Python 3.12.14. Its six cases exercise accepted objects, decimal representation, duplicate detection, non-finite constants, and the root-type rule.
Check ambiguity before business meaning
The object hook receives the member pairs before this function constructs its dictionary. Each nested object goes through the same hook. A repeated x in a child object is therefore rejected even when the outer object has unique names.
After this boundary, application validation still has work to do. The parser accepts an object with a single decision value of "launch spacecraft"; it does not define the business vocabulary. It also does not establish whether an integer is a valid identifier or whether a decimal lies within an allowed range.
Keep those stages separate in tests. One test can ask whether the document is unambiguous under your decoding policy. Another can ask whether the decoded record satisfies your domain contract. Their failure messages can then tell the caller which decision failed.
State what the size check cannot guarantee
The string already exists in memory before this function checks its encoded length. This is not a bounded network reader. It does not impose a nesting-depth limit, a numeric-digit policy, or a total CPU budget. A production boundary needs limits appropriate to its transport and parser.
The function deliberately raises an error without echoing the entire input. That keeps this particular exception message small; it is not a comprehensive logging or secret-redaction guarantee.
The practical lesson is about preserving a validation opportunity. Once decoding has collapsed repeated names into one dictionary entry, a later schema check may be too late to ask whether the original document supplied one answer or several competing answers.
來源:DEV Community · dev.to