Quick answer: Test five independent boundaries around Python Requests JSON responses: status acceptance, body presence, declared media type, decoded value shape, and custom decoder policy. Response.json is a decoding operation, not proof of a successful status or usable application data. Keep each failure distinguishable so fixes target the right layer.
Your integration calls response.json and gets a dictionary, then fails later because that dictionary describes an upstream error. The Python JSON module guide covers serialization basics; these tests focus on the Requests response boundary rather than making a live service call.
Run the blocks together, in order. They were tested with Python 3.12.14 and Requests 2.34.2, using the standard-library JSON backend and synthetic preloaded Response objects. No DNS, TLS, timeout, redirect, compression, or remote server behavior was exercised. Wikipedia's HTTP overview supplies protocol background; the primary Requests references define library behavior.
1. Does valid JSON remain separate from the endpoint's accepted HTTP status?
Test a JSON error document on an unsuccessful status and a valid document on an explicitly accepted status. Response.json can decode either body. Use raise_for_status for HTTP error responses, then apply a narrower allowed-status rule when the endpoint contract needs it. Successful decoding does not turn an error document into a successful operation.
The Requests quickstart distinguishes decoding from response success. The fixture below keeps the same valid JSON syntax while changing status, so a parser failure cannot accidentally become the reason an error response is rejected.
import requests
from decimal import Decimal
assert requests.__version__ == "2.34.2"
def response(status, body, content_type="application/json"):
result = requests.Response()
result.status_code = status
result.url = "https://example.invalid/fixture"
result._content = body
result._content_consumed = True
result.encoding = "utf-8"
if content_type is not None:
result.headers["Content-Type"] = content_type
return result
failed = response(500, b'{"error":"upstream_unavailable"}')
assert failed.json() == {"error": "upstream_unavailable"}
try:
failed.raise_for_status()
except requests.exceptions.HTTPError as error:
assert error.response is failed
else:
raise AssertionError("HTTP failure was accepted")
accepted = response(200, b'{"id":"item-42"}')
accepted.raise_for_status()
assert accepted.status_code == 200
assert accepted.json() == {"id": "item-42"}
pending = response(202, b'{"state":"pending"}')
pending.raise_for_status()
assert pending.status_code != 200
The final control matters when an endpoint promises a completed result only on 200. A 202 response may be a legitimate asynchronous state, but it is not the same completed-result contract. raise_for_status does not express every application-specific status restriction; write that policy where the caller decides what to do next.
Preserve safe response evidence when status handling fails. The exception's response reference helps diagnostics retain status and selected headers without dumping a full customer body. If you also parse structured error details, do that through a deliberate error-body path rather than accidentally passing the decoded error object to normal success handling.
Keep retry decisions outside the JSON decoder. A 500 document, a 429 document, and an unexpected success status can all contain valid JSON while requiring different handling. The rate-limit guide covers one related policy; this local fixture proves none of the network timing or retry behavior.
Do not use response truthiness to decide whether an exception contains a response. Requests response objects can evaluate false for error statuses, precisely when you most need their context. The explicit identity assertion in this fixture avoids that ambiguity. In application handling, check whether the response reference is None rather than treating its boolean value as proof that no response arrived. That keeps a received error separate from a connection failure with no response object.
2. Does an absent body follow an explicit policy instead of a dictionary fallback?
Test a bodyless success separately from an empty body where JSON was promised, and include a nonempty malformed document. Requests raises JSONDecodeError when Response.json cannot decode the content. Do not silently replace every decode failure with an empty dictionary, because that merges legitimate no-content responses with missing or corrupted upstream data.
The Requests decoder API documents the decode exception. A no-content status does not make an empty byte sequence a JSON object. The caller must decide whether that response state is an expected result for the particular operation.
empty = response(204, b"", content_type=None)
missing_document = response(200, b"")
malformed = response(200, b'{"id":')
for item in [empty, missing_document, malformed]:
try:
item.json()
except requests.exceptions.JSONDecodeError:
pass
else:
raise AssertionError("Undecodable content unexpectedly decoded")
assert empty.status_code == 204 and empty.content == b""
explicit_object = response(200, b"{}")
assert explicit_object.json() == {}
explicit_null = response(200, b"null")
assert explicit_null.json() is None
The explicit object and null controls show why a fallback is misleading. An empty dictionary is a real decoded value with its own meaning, and null is another real value. Neither is equivalent to receiving no document at all. Returning a single fallback for all three loses evidence the caller may need to distinguish an upstream contract change from a normal result.
For deletion or update endpoints that legitimately return no body, handle the accepted bodyless state before calling the decoder. For an endpoint that promises a result document, treat an empty success body as its own failure. Test the method and status combinations your real service uses instead of making a universal rule that all successful responses must contain JSON.
Catch the Requests exception type at the library boundary, and keep custom validation failures separate. Broad except Exception handlers around networking, decoding, and field access can turn an unexpected programming defect into an apparent empty result. Narrow tests make the intended fallback, if any, visible and reviewable.
Keep a whitespace-only body in the endpoint's regression corpus when intermediaries can return it. It is nonempty as bytes but still contains no JSON value, so checking content length alone is insufficient. A syntax error and an unexpectedly empty response may share the decoder's exception category while needing different operational investigation. Classify them using safe response evidence rather than rewriting either as a normal empty result.
3. Does the client enforce its media-type contract independently of decoding?
Test valid JSON under an unexpected or missing Content-Type, and invalid content under a JSON header. Requests decoding is not your media-type allowlist. Normalize the declared base media type for comparison, decide which exact types the endpoint supports, and then decode separately. A trusted-looking header cannot make HTML into JSON or prove the body matches an application schema.
The Requests response implementation shows decoding behavior. This fixture deliberately sets UTF-8 encoding and preloaded content, isolating header-policy decisions from character-encoding detection and network delivery.
def accepted_media_type(item):
declared = item.headers.get("Content-Type", "")
base = declared.split(";", 1)[0].strip().lower()
return base in {"application/json", "application/problem+json"}
wrong_label = response(200, b'{"id":"item-42"}', "text/html")
assert wrong_label.json() == {"id": "item-42"}
assert not accepted_media_type(wrong_label)
assert not accepted_media_type(response(200, b"{}", None))
assert accepted_media_type(response(200, b"{}", "Application/JSON; charset=utf-8"))
assert accepted_media_type(response(400, b"{}", "application/problem+json"))
html = response(200, b"<html>maintenance</html>", "application/json")
assert accepted_media_type(html)
try:
html.json()
except requests.exceptions.JSONDecodeError:
pass
else:
raise AssertionError("JSON header incorrectly established valid JSON")
The helper is an explicit two-type example policy, not a complete MIME parser or a statement that every API must accept problem documents on its success path. Your service may use another documented JSON media type. Add that exact type deliberately rather than accepting arbitrary substrings containing json or assuming every suffix implies an acceptable endpoint contract.
A missing header and a mislabeled body can suggest an upstream proxy, authentication page, or server configuration issue, but these fixtures do not identify the actual cause of a production incident. Preserve status and a safe header summary before investigating. The content-type guide explains the broader header boundary without treating it as syntax validation.
Do not paste an entire failed upstream response into an error message sent to end users. HTML authentication pages, JSON error details, and response headers can contain identifiers or secrets. Use synthetic examples in tests and choose a documented redaction policy for operational evidence. Retaining enough context to distinguish a header mismatch from a parse failure does not require retaining every byte of the rejected response.
4. Does the decoded root have the type and fields the caller actually needs?
Test valid JSON roots that are not objects, then test an object missing the required field and an object with an unacceptable field value. Response.json can return any decoded JSON shape. Validate that shape before object-specific access, and keep application-contract failures distinct from decoding failures so syntactically valid but unusable data is reported accurately.
The Requests Response.json reference describes returning Python values according to the body. The parser does not infer that your lookup operation expects an object containing a nonempty string identifier.
def require_item(value):
if not isinstance(value, dict):
raise ValueError("Expected item object")
if not isinstance(value.get("id"), str) or not value["id"].strip():
raise ValueError("Expected nonempty string id")
return value
for body in [b"[]", b"42", b'"hello"', b"false", b"null", b"{}", b'{"id":0}']:
decoded = response(200, body).json()
try:
require_item(decoded)
except ValueError:
pass
else:
raise AssertionError("Unexpected application shape accepted")
assert require_item(response(200, b'{"id":"item-42"}').json())["id"] == "item-42"
This helper checks only the two claims shown. It does not reject extra fields, validate related records, or establish authorization. For a larger contract, use a schema or model with explicitly configured behavior and preserve the same negative fixtures. The JSON Schema guide covers that separate validation layer.
Use the JSON Viewer to inspect a sanitized valid response fixture when confirming whether the root is an array, object, or scalar. Inspection helps locate data; it does not prove HTTP success, execute this Python helper, or establish that every required business field is present.
A response containing an empty string identifier also deserves its own fixture when blank values are forbidden. Presence alone is not value quality, and a key with zero or null is not the same as a missing key. Name these failures separately if the calling code offers different recovery guidance. Keep the accepted identifier fixture as a positive control so a validator that rejects every response cannot make the rejection suite appear successful.
5. Do decoder options enforce the chosen numeric and duplicate-name policy?
Test the exact keyword arguments passed to Response.json, including accepted numeric conversion and a duplicate-name rejection hook. Requests forwards decoder options, but a custom hook can raise its own policy exception. Verify that exception independently from malformed JSON, and record the installed backend because optional decoder dependencies can affect which behavior your environment actually exercises.
The Requests API documents decoder keyword arguments. The Python JSON decoder documentation defines the standard-library options used in this environment. A response library wrapper does not automatically choose your application's precision or duplicate-name policy.
precise = response(200, b'{"amount":19.90}').json(parse_float=Decimal)
assert precise["amount"] == Decimal("19.90")
assert isinstance(precise["amount"], Decimal)
def unique_pairs(pairs):
result = {}
for name, value in pairs:
if name in result:
raise ValueError("duplicate response name")
result[name] = value
return result
repeated = response(200, b'{"id":"first","id":"second"}')
assert repeated.json() == {"id": "second"}
try:
repeated.json(object_pairs_hook=unique_pairs)
except ValueError as error:
assert not isinstance(error, requests.exceptions.JSONDecodeError)
assert str(error) == "duplicate response name"
else:
raise AssertionError("Duplicate-name policy did not run")
print("All five Requests response test groups passed")
The Python decoding policy tests expand these choices beyond one response wrapper. Here the important boundary is that the options reach the actual Response.json call and that policy errors remain distinguishable in the caller's diagnostics.
Keep these five groups beside the response adapter, then add real integration checks for the transport behavior your service depends on. Record status, body expectations, media type, decoded shape, and decoder policy separately. That evidence makes a failure actionable without pretending a successful JSON parse proves that the entire HTTP operation succeeded.
When changing dependency versions, run the fixtures in the deployment environment as well as a developer shell. Record whether an optional JSON backend is installed and whether your wrapper supplies the same decoder options. The test results here establish behavior for the stated pinned setup; they are not a claim about every Requests dependency combination. Preserve the original synthetic bytes with each expected result so future failures can be reproduced without a live external API.