`numpy.float64`の値は`json.dumps`で出力できるが、`numpy.int64`は`TypeError`が発生する

# 環境
* Python 3.11.2
* pandas 2.0.1
* numpy 1.24.2


# はじめに
`pandas.DataFrame`から取得した情報を加工して、JSON出力したいです。

以下のPythonスクリプトを実行すると、`json.dumps`で"TypeError: Object of type int64 is not JSON serializable"というエラーが発生しました。


```python:sample.py
import pandas
from dataclasses import dataclass, asdict
import json


@dataclass
class Info:
    year: int
    score: int


df = pandas.DataFrame({"year": [2020, 2021, 2022], "score": [2, 4, 6]})
print(df.dtypes)

last_row = df.iloc[-1]
last_info = Info(last_row["year"], last_row["score"])

# last_infoを使って何かする
...
result = last_info
print(f"{type(result.year)=}, {type(result.score)=}")
print(json.dumps(asdict(result)))
```

```
$ python sample.py
year     int64
score    int64
dtype: object
type(result.year)=<class 'numpy.int64'>, type(result.score)=<class 'numpy.int64'>
Traceback (most recent call last):
  File "/home/vagrant/Documents/study/20230511/sample.py", line 25, in <module>
    print(json.dumps(asdict(result)))
          ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vagrant/.pyenv/versions/3.11.2/lib/python3.11/json/__init__.py", line 231, in dumps
    return _default_encoder.encode(obj)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vagrant/.pyenv/versions/3.11.2/lib/python3.11/json/encoder.py", line 200, in encode
    chunks = self.iterencode(o, _one_shot=True)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vagrant/.pyenv/versions/3.11.2/lib/python3.11/json/encoder.py", line 258, in iterencode
    return _iterencode(o, 0)
           ^^^^^^^^^^^^^^^^^
  File "/home/vagrant/.pyenv/versions/3.11.2/lib/python3.11/json/encoder.py", line 180, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type int64 is not JSON serializable
```


# エラーの原因
`numpy.int64`は`int`のサブクラスでないから、`json.dumps`で`TypeError`が発生しました。
もっとシンプルなコードで確認してみます。

```
In [27]: json.dumps(numpy.int64(1))
---------------------------------------------------------------------------
...
TypeError: Object of type int64 is not JSON serializable

In [28]: issubclass(numpy.int64, int)
Out[28]: False
```


## `numpy.int`が`int`のサブクラスでない理由
Pythonの整数は上限がなく65bit以上の整数を表現できます。一方`numpy.int64`は64bitまでしか整数を表現できません。したがって、`numpy.int64`は`int`のサブクラスではありません。

https://stackoverflow.com/questions/51186804/

https://note.nkmk.me/python-int-max-value/

https://zenn.dev/yukinarit/articles/afb263bf68fff2

# `numpy.int64`以外はJSON出力できるのか？
`json.dumps` / `json.dump`は以下の[変換表](https://docs.python.org/ja/3/library/json.html#py-to-json-table)に従って処理します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37903/0cad278b-9b0a-63b3-ab7a-b6183af15b21.png)

`numpy.int64`以外のnumpyの型が、JSON出力できるかを確認しました。


## `numpy.float64`

```
In [84]: issubclass(numpy.float64, float)
Out[84]: True

In [85]: json.dumps(numpy.float64(1.2))
Out[85]: '1.2'
```

Pythonのfloatは64bitで、`numpy.float64`と表現できるbit数が同じです。だから`numpy.float64`は`float`のサブクラスになれているのかもしれません。
※`numpy.float64`が`float`のサブクラスになっている明確な理由は分かりませんが、きっとサブクラスにした方がが便利だからでしょう。

https://github.com/numpy/numpy/issues/17283

https://note.nkmk.me/python-sys-float-info-max-min/

## `numpy.bool_`

```
In [86]: issubclass(numpy.bool_, bool)
Out[86]: False

In [87]: issubclass(bool, int)
Out[87]: True
```

`bool`は`int`クラスのサブクラスです。したがって、`numpy.bool_`は`bool`のサブクラスではありませんでした。

>bool クラスは int クラスの派生クラスです (数値型 int, float, complex を参照してください)。このクラスからさらに派生することはできません。[^1]

[^1]: https://docs.python.org/ja/3/library/functions.html#bool

## `numpy.str_`


```
In [88]: issubclass(numpy.str_, str)
Out[88]: True

In [89]: json.dumps(numpy.str_("a"))
Out[89]: '"a"'
```


## `numpy.ndarray`

```
In [100]: issubclass(numpy.ndarray, list)
Out[100]: False
```

# 上述のエラーを解決する方法

## (a) `int()`関数で`int`に変換する

```python
last_info = Info(int(last_row["year"]), int(last_row["score"]))
```

## (b) pandasの`to_dict`や`to_json`を利用する

```
In [160]: last_row.to_json()
Out[160]: '{"year":2022,"score":6}'

In [161]: json.dumps(last_row.to_dict())
Out[161]: '{"year": 2022, "score": 6}'
```

## (c) `json.dumps`関数の`default`引数に、変換する処理が記載された関数を指定する
`default`引数に指定した関数は、Serializeできないオブジェクトに対して呼ばれます。

```python:sample1.py
import numpy
import json


def default(o):
    print(f"{type(o)=}")
    if isinstance(o, numpy.int64):
        return int(o)
    elif isinstance(o, numpy.bool_):
        return bool(o)
    elif isinstance(o, numpy.ndarray):
        return list(o)
    raise TypeError(repr(o) + " is not JSON serializable")


print(json.dumps(numpy.int64(1), default=default))
print(json.dumps(numpy.bool_(True), default=default))
print(json.dumps(numpy.array([1.2, 2.3]), default=default))
```

```
$ python sample1.py 
type(o)=<class 'numpy.int64'>
1
type(o)=<class 'numpy.bool_'>
true
type(o)=<class 'numpy.ndarray'>
[1.2, 2.3]
```

## (d) `json.dumps`関数の`cls`引数に、`JSONEncoder`のサブクラスを指定する

```pythonimport numpy
import json
from json import JSONEncoder

class NumpyJSONEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, numpy.int64):
            return int(o)
        elif isinstance(o, numpy.bool_):
            return bool(o)
        elif isinstance(o, numpy.ndarray):
            return list(o)
        return JSONEncoder.default(self, o)

print(json.dumps(numpy.int64(1), cls=NumpyJSONEncoder))
print(json.dumps(numpy.bool_(True), cls=NumpyJSONEncoder))
print(json.dumps(numpy.array([1.2, 2.3]), cls=NumpyJSONEncoder))
```


```
$ python sample2.py 
1
true
[1.2, 2.3]
```

# 参考にしたサイト
https://xmadoka.hatenablog.com/entry/2019/02/23/011913

https://qiita.com/podhmo/items/dc748a9d40026c28556d