HTTP and JSON are separate contracts
How to study this lesson
Read one section, trace the worked example, and try the knowledge check. Bookmark saves a shortcut; notes appear in My notebook. Mark lesson complete is your own assessment and does not mark its lab passed.
By the end, you can
- Distinguish transport success, JSON syntax, and domain validity.
- Validate exact types and domain ranges.
- Design errors that callers can act on.
A successful connection is only the first gate
An HTTP exchange has several layers of meaning. The connection might fail before a response exists. A response can arrive with a non-success status. A success status can carry invalid JSON. Valid JSON can violate your application's contract. A robust caller checks each boundary in that order and preserves enough context to explain which one failed.
Suppose a pricing service returns status 200 and the body {"price_cents": "1200"}. Transport succeeded, and the JSON is syntactically valid, but the price is a string. Silently converting it might hide an upstream regression. Decide whether coercion is supported by the contract. At a money-related boundary, an explicit integer field and a rejected mismatch make downstream arithmetic easier to audit.
JSON has values, your application has meaning
JSON describes objects, arrays, strings, numbers, booleans, and null. It does not encode a Python dataclass, a date object, a decimal policy, or a unit. A field called duration is incomplete until its unit and valid range are defined. JSON numbers also travel between runtimes with different number representations, so large identifiers are often better represented as strings.
Python type annotations help editors and static checkers but do not enforce runtime inputs. A parameter annotated as int can still receive a string. Also, bool is a subclass of int in Python, so isinstance(True, int) is true. If true is not a valid item count, use an exact type check or a validator whose strictness you have verified. This small detail frequently breaks seemingly obvious boundary checks.
Make one canonical representation
Normalize only after establishing what transformations the contract permits. For an item identifier, stripping surrounding whitespace might be acceptable; lowercasing might not be, because identifiers can be case-sensitive. Reject empty normalized identifiers and unexpected fields when ambiguity is expensive. A canonical internal object should make later code simpler, rather than requiring every function to repeat defensive checks.
For example, an incoming quantity of 3 and unit price of 250 cents yields total_cents = 750. This computation belongs after validation, where quantity is a positive integer and price is a nonnegative integer. The internal total should not depend on whether the caller used pretty-printed JSON or inserted spaces. Canonicalization removes irrelevant representation differences without inventing missing business facts.
Errors should preserve categories
A useful boundary can report invalid_json, invalid_shape, invalid_field, and service_unavailable separately. These names tell a caller whether to fix input, contact a dependency, or try again later. Include a safe field path and a request identifier. Avoid returning the full raw body when it may contain secrets or user data, and avoid exposing a server traceback to an untrusted client.
There is a tradeoff between collecting all validation errors and stopping at the first one. A form benefits from showing several correctable fields at once. A streaming ingestion job may prefer immediate rejection for each malformed record. Pick one behavior deliberately and make it stable so clients do not depend on accidental exception wording from a particular Python release.
Use fixtures to separate contract from network
The example receives an in-memory response body, validates exact keys and types, and computes a total. This keeps the important boundary deterministic. Later, an HTTP adapter can pass its decoded body into the same function. You can then test transport retry behavior independently from schema behavior instead of needing a live remote service to reproduce a boolean quantity bug.
For a production boundary, also specify content-type handling, body-size limits, numeric bounds, unknown-field policy, and schema versioning. Python's JSON parser is not a complete JSON Schema implementation, and the hand-written validator here intentionally supports one small object. Once contracts become nested or shared across languages, use a maintained schema tool, while keeping domain checks such as inventory limits in ordinary application code.
Work through the code
An in-memory JSON string becomes a validated line item and an integer total. The second fixture proves that a JSON boolean is rejected as a quantity. This demonstrates a narrow contract, not a general JSON Schema engine. Add an explicit maximum before using quantities in resource allocation.
import json
def parse_line(body):
value = json.loads(body)
if not isinstance(value, dict):
raise ValueError("object required")
if set(value) != {"sku", "quantity", "price_cents"}:
raise ValueError("unexpected fields")
if not isinstance(value["sku"], str) or not value["sku"].strip():
raise ValueError("invalid sku")
for field in ("quantity", "price_cents"):
if type(value[field]) is not int:
raise ValueError(f"{field} must be an integer")
if value["quantity"] <= 0 or value["price_cents"] < 0:
raise ValueError("invalid range")
return {"sku": value["sku"].strip(),
"total_cents": value["quantity"] * value["price_cents"]}
print(parse_line('{"sku":" A7 ","quantity":3,"price_cents":250}'))
try:
parse_line('{"sku":"A7","quantity":true,"price_cents":250}')
except ValueError as error:
print("rejected:", error)
{'sku': 'A7', 'total_cents': 750}
rejected: quantity must be an integerRun Python snippets locally with the prerequisites named above. The module coding lab runs directly in your browser.
Pause and reason
A client sends {"sku":"A7","quantity":2,"price_cents":500,"discount":90}. Your server ignores discount and charges 1000 cents. Explain the contract risk and choose an unknown-field policy.
Check your understanding
Which check rejects JSON true when a field must contain an integer count?
Your notes
Explain the mechanism in your own words. Add a failure you want to test.