TypeError: the JSON Object Must Be str, bytes or bytearray (tested)

json.loads() only accepts text: a str, bytes, or bytearray. Anything else raises TypeError: the JSON object must be str, bytes or bytearray, not X, naming whatever type you actually passed. We measured the four things people pass by mistake.

1. The classic mistake: a file object instead of its contents

import json

with open("data.json") as f:
    json.loads(f)
TypeError: the JSON object must be str, bytes or bytearray, not TextIOWrapper

with open("data.json") as f:
    json.load(f)   # correct: note load(), not loads()
{'a': 1}

Measured: this is the single most common cause — confusing json.load() (takes a file object, reads it internally) with json.loads() (takes a string you already have). The error message names the exact wrapper type, TextIOWrapper, which is the giveaway that a file handle was passed where text was expected.

2. None — usually from a lookup that silently failed

import os
val = os.environ.get("NONEXISTENT_VAR")
json.loads(val)
TypeError: the JSON object must be str, bytes or bytearray, not NoneType

Measured: this fires whenever the value feeding json.loads() turns out to be None — a dict's .get() with no default, an environment variable that isn't set, or an HTTP client's .text/.content attribute being None on an empty or failed response. The NoneType in the message is the signal that something upstream returned nothing when you expected a string.

3. A dict that's already parsed (double-parsing)

d = {"a": 1}
json.loads(d)
TypeError: the JSON object must be str, bytes or bytearray, not dict

Measured: this happens when code calls json.loads() on data that's already a Python object — for example, a library (like requests' response.json()) that already returns a parsed dict, and a second, unnecessary json.loads() call gets applied on top of it.

4. Any other object without special handling

class FakeResponse:
    def read(self): return b"{}"

json.loads(FakeResponse())
TypeError: the JSON object must be str, bytes or bytearray, not FakeResponse

Measured: a response-like object with its own .read() or .text method isn't automatically unwrapped — json.loads() has no idea how to extract text from an arbitrary object. You have to call the right accessor yourself first (.read(), .text, .json(), whatever the library provides) and pass that result in.

5. bytes and bytearray both work fine — this isn't a strict str-only check

json.loads(b'{"a": 1}')
{'a': 1}

Measured: despite the name, this accepts raw bytes directly (UTF-8, UTF-16, or UTF-32, auto-detected by BOM or null-byte pattern) without needing .decode() first — so if you're getting this error, the input genuinely isn't text or bytes at all, not just the wrong encoding.

6. How to read the message to find the fix

The type named at the end of the message tells you exactly what to do: TextIOWrapper (or any file-like type) means swap json.loads() for json.load(), or call .read() first; NoneType means trace back why the value upstream is empty before it ever reaches json.loads(); dict (or list) means the data was already parsed and the call is redundant; anything else means you need to extract the actual text/bytes from that object's own API first.

Frequently asked questions

Why do I get "not TextIOWrapper" when parsing a file?

We confirmed this means a file object was passed to json.loads() instead of json.load() — loads() expects a string you already have in memory, while load() takes the file object itself and reads it internally.

Why does json.loads(None) raise this error?

Because we measured that None isn't str, bytes, or bytearray — this surfaces whenever an upstream lookup silently returns nothing, such as dict.get() with no default, an unset environment variable, or an empty HTTP response body.

Can json.loads() accept bytes directly, or does it need .decode() first?

It accepts bytes and bytearray directly — we measured json.loads(b'{"a": 1}') working with no decode() call, auto-detecting UTF-8/16/32 by BOM or byte pattern, so this error means the input truly isn't text or bytes at all.

Why would json.loads() complain about a dict?

We confirmed this happens when data is already parsed — often because a library like requests' response.json() already returns a dict, and a redundant second json.loads() call gets applied to that already-parsed result.

Does json.loads() automatically call .read() on a response-like object?

No — we tested a custom object with its own .read() method and json.loads() did not call it automatically; you have to extract the actual text or bytes yourself using whatever accessor that object's own API provides, then pass the result in.

Written with AI assistance. Measured on 2026-10-03 with Python 3.13.5's built-in json module, calling json.loads() on several non-string inputs and printing the exact exception text. Every message below is copy-pasted output, not paraphrased. PlainHeaders is our extension; Python's json module is built in.