Defensive JSON Parsing: Validate Shape, Presence and Exact Quantities

한국어로 읽기

TECH INSIGHT / EXPLAINED

Successful JSON decoding establishes syntax, not a valid quote or order result. Build a boundary that distinguishes HTTP status, response shape, provider success codes and the domain value you intend to use.

Three colleagues validate JSON structure and numeric values before allowing data into an application.
AI-generated educational illustration. Three colleagues validate JSON structure and numeric values before allowing data into an application.

Local execution of the actual project

These captures show the existing project UI or functions executed locally with non-private sample inputs. Each caption states the scope of the execution.

An execution report for missing fields, arrays, null, and numeric success codes passed to the original KISClient._parse. Missing rt_cd passes, while arrays and null raise AttributeError. These are findings in the existing project implementation, separate from the article’s defensive validation example.
An execution report for missing fields, arrays, null, and numeric success codes passed to the original KISClient._parse. Missing rt_cd passes, while arrays and null raise AttributeError. These are findings in the existing project implementation, separate from the article’s defensive validation example.

Validate the envelope before extracting fields

With Requests, inspect HTTP status and catch requests.exceptions.JSONDecodeError around response.json(). An empty successful response may be valid for one endpoint and invalid for another. A JSON list, null or HTML error page requires an endpoint-specific decision.

The pattern (output or {}) handles null only in some shapes; it still fails for a nonempty list or string and can hide invalid false values. Require a dictionary before .get(). Do not accept a missing rt_cd as success when that field is required by the endpoint.

Reject fractional quantities instead of truncating them

int(float(value)) turns "12.9" into 12 and can corrupt large integers. The following offline example allows integer-valued decimal strings such as "12.0" while rejecting booleans, fractional values, NaN and infinity. It raises a validation exception so the caller must choose a recovery policy explicitly.

import json
from decimal import Decimal, InvalidOperation

def reject_constant(value):
    raise ValueError("non-standard JSON numeric constant")

def whole_quantity(value):
    if type(value) not in (str, int, Decimal):
        raise ValueError("quantity has the wrong type")
    try:
        number = Decimal(value)
    except InvalidOperation as exc:
        raise ValueError("invalid quantity") from exc
    if not number.is_finite() or number < 0 or number != number.to_integral_value():
        raise ValueError("quantity must be a finite nonnegative integer")
    return int(number)

def parse_example(raw):
    # Example contract, not a universal exchange response schema.
    if len(raw) > 1_048_576:
        raise ValueError("response exceeds this application's size limit")
    body = json.loads(raw, parse_float=Decimal, parse_constant=reject_constant)
    if not isinstance(body, dict) or body.get("rt_cd") != "0":
        raise ValueError("missing success envelope or business failure")
    output = body.get("output")
    if not isinstance(output, dict) or "qty" not in output:
        raise ValueError("missing output quantity")
    return whole_quantity(output["qty"])

assert parse_example('{"rt_cd":"0","output":{"qty":"12.0"}}') == 12
assert whole_quantity("9007199254740993") == 9007199254740993

Choose the operational response deliberately

For an optional dashboard field, absence may be acceptable. For price, order quantity or reconciliation state, record a structured validation error and prevent decisions based on invalid data. Do not turn every bad number into zero or skip indefinitely without an alert.

Log the endpoint name, status, safe error category and correlation ID. A body prefix can still contain a token, account data or reflected request text; truncation is not redaction. A retry depends on the operation and error semantics. An unknown order outcome needs reconciliation, while some business errors may be transient.

Go needs domain checks too

encoding/json can reject a valid JSON document when its types do not match the destination. In a newly zeroed struct, absent fields remain zero; reused structs may retain earlier values. Pointer fields distinguish a numeric zero from nil, but missing and explicit null both become nil in a fresh pointer field. Use an explicit presence model if that distinction matters.

Reject non-finite parsed numbers with math.IsNaN/math.IsInf before price comparisons. For delimiter feeds, validate the message version, field count and documented field types before indexing; one hardcoded count or six-character symbol rule is not universal. Schema libraries help with repeated complex contracts, but endpoint count alone does not determine whether they are needed.

Sources and further reading

Requests JSON errors · Python JSON decoder · Go encoding/json

Content and examples reviewed: 2026-09-29

댓글