TypeError: Object of type int64 is not JSON serializable" in PythonI hit this the first time on a Flask endpoint that had been returning fine for weeks, then broke the day a groupby result got passed straight into the response dict instead of a plain Python count. json.dumps() raises the moment it reaches a dict containing a NumPy value. Some NumPy types trip it and others don't, which I only figured out by testing each one directly (below) rather than trusting the first Stack Overflow answer I found. The obvious fix (a custom encoder) also turned out to only be half the fix, since it silently does nothing for NumPy values used as dict keys.
The most common trigger is a dict that mixes plain Python values with something pulled straight out of NumPy or pandas -- a count, a model prediction, an array -- without converting it first:
import json
import numpy as np
data = {"count": np.int64(5), "label": "cat"}
json.dumps(data)
# TypeError: Object of type int64 is not JSON serializable
The error names whichever NumPy type it actually hit, so it's not always literally "int64" -- the same failure shows up for float32, bool_, and raw ndarray objects with their own type name in the message.
Not every NumPy scalar type has this problem, and I couldn't find a source that actually tested each one, so I ran them directly against NumPy 2.5.1:
import json
import numpy as np
json.dumps(np.float64(3.14)) # works: '3.14' -- float64 IS a subclass of float
print(isinstance(np.float64(3.14), float)) # True
json.dumps(np.int64(5)) # TypeError -- int64 is NOT a subclass of int
json.dumps(np.float32(3.14)) # TypeError -- float32 is NOT a subclass of float
json.dumps(np.bool_(True)) # TypeError -- bool_ is NOT a subclass of bool
json.dumps(np.array([1, 2])) # TypeError -- ndarray was never a candidate
np.float64 is implemented as an actual Python float subclass, so the standard json module recognizes it without any help. int64, int32, float32, and bool_ don't subclass their Python equivalents, so all four hit the same TypeError as a plain ndarray does. That's the whole reason my endpoint broke where it did: the specific computation that changed produced an int64 count instead of the float64 average it had been returning before.
The standard fix is a small JSONEncoder subclass that converts each NumPy type json doesn't already understand, passed via cls= (or the equivalent inline default= function):
import json
import numpy as np
class NumpyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, np.integer):
return int(obj)
if isinstance(obj, np.floating):
return float(obj)
if isinstance(obj, np.ndarray):
return obj.tolist()
if isinstance(obj, np.bool_):
return bool(obj)
return super().default(obj)
data = {"count": np.int64(5), "scores": np.array([0.9, 0.85]), "flag": np.bool_(True)}
print(json.dumps(data, cls=NumpyEncoder))
# {"count": 5, "scores": [0.9, 0.85], "flag": true}
np.integer and np.floating are NumPy's own abstract base classes covering every fixed-width integer and float type (int8 through int64, float16 through float64), so this doesn't need a separate branch per exact dtype. This fixes every value in the example above.
NumpyEncoder above does nothing for a NumPy value used as a dict key -- this is what got me even after adding the encoder above.I ran into it building a label-count dict directly from np.unique, which is a common enough pattern:
labels = np.array([1, 1, 2, 2, 2, 3])
values, counts = np.unique(labels, return_counts=True)
label_counts = dict(zip(values, counts))
print({type(k) for k in label_counts.keys()}) # {<class 'numpy.int64'>}
json.dumps(label_counts, cls=NumpyEncoder)
# TypeError: keys must be str, int, float, bool or None, not numpy.int64
The reason the encoder can't help here is structural: JSONEncoder.default() is only ever called for values the encoder doesn't already know how to serialize. Dict keys go through a separate internal coercion step (deciding whether a key becomes a JSON string) that never calls default() at all -- so no amount of extending the encoder fixes a NumPy key. It has to be converted before the dict reaches json.dumps().
For nested structures, or any dict that might have NumPy keys, converting everything to native Python types first -- recursively, keys included -- is the fix that actually covers both problems at once:
def to_native(obj):
if isinstance(obj, dict):
return {to_native(k): to_native(v) for k, v in obj.items()}
if isinstance(obj, (list, tuple)):
return [to_native(v) for v in obj]
if isinstance(obj, np.ndarray):
return obj.tolist()
if isinstance(obj, np.integer):
return int(obj)
if isinstance(obj, np.floating):
return float(obj)
if isinstance(obj, np.bool_):
return bool(obj)
return obj
json.dumps(to_native(label_counts))
# '{"1": 2, "2": 3, "3": 1}'
The keys come out as JSON strings ("1") even though to_native converts them to a Python int first. JSON object keys are always strings; json.dumps stringifies any int/float/bool key on its own once the key is a type it recognizes. So to_native only has to get the key to a type json already knows how to handle -- it doesn't need to produce the final string itself.
For a single value with no nesting, .item() is a shorter equivalent that converts any NumPy scalar to its native Python type in one call: np.int64(5).item() returns a plain int, and works the same way for every scalar type in this article.
If the object being serialized is already a pandas DataFrame or Series -- not a plain dict that happens to contain a few NumPy scalars pulled out of one -- skip both fixes above and use pandas' own .to_json(), which handles NumPy dtypes natively:
import pandas as pd
df = pd.DataFrame({"count": [np.int64(5), np.int64(3)]})
print(df.to_json())
# {"count":{"0":5,"1":3}} -- no error, no custom encoder needed
This only helps for the DataFrame/Series object itself, though -- if a NumPy scalar has already been pulled out into a plain dict (e.g. df["count"].sum() stored alongside other fields), that dict still needs one of the two fixes above.
If I'm serializing a DataFrame or Series directly, .to_json() skips this problem entirely and I don't bother with either fix. For a plain dict with NumPy values but no NumPy keys, the NumpyEncoder from Fix 1 is enough and reads a bit cleaner at the call site (json.dumps(data, cls=NumpyEncoder)) than wrapping every call in a conversion function. The moment a NumPy value could end up as a key -- anything built from np.unique, zip() over an array, or a groupby result -- I skip straight to the recursive to_native from Fix 2, since debugging why a working encoder still raises TypeError on keys cost me more time than just converting everything up front would have. For one bare scalar with nothing nested around it, .item() is shorter than either.