`pandas.DataFrame.to_json`はスラッシュをエスケープして出力する

# 環境
* Python 3.12.1
* pandas 2.2.2


# はじめに
[pandas.DataFrame.to_json()](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_json.html#pandas.DataFrame.to_json)は、スラッシュ`/`を`\\`でエスケープして出力します。


```
In [6]: import pandas as pd

In [12]: data = [{"id":"alice", "url":"https://example.com"}]

In [16]: df = pd.DataFrame(data)

In [17]: df.to_json()
Out[17]: '{"id":{"0":"alice"},"url":{"0":"https:\\/\\/example.com"}}'
```

しかし、`json.dumps`ではスラッシュをエスケープせずに出力します。

```
In [15]: json.dumps(data)
Out[15]: '[{"id": "alice", "url": "https://example.com"}]'
```

この違いは何なのでしょうか？

# JSONの仕様
JSONの仕様では、スラッシュをエスケープすることができます。ただし、スラッシュのエスケープは必須でありません。

https://blog.magnolia.tech/entry/2021/01/18/212758

>ここで一つポイントなのは、いくつかの実装でsolidus(スラッシュ)がエスケープの対象でなかったり、オプションだったりすること。JavaScriptの規格であるECMA-262ではエスケープ表記の対象にsolidus(スラッシュ)は含まれていなかったけど、最初の独立したJSON規格であるRFC 4627以降では対象に含まれている、という違いが有る。


なお、`json`モジュールはスラッシュをエスケープしてエンコードしませんが、エスケープされたスラッシュをデコードできます。


```
In [19]: tmp = df.to_json()

In [20]: tmp
Out[20]: '{"id":{"0":"alice"},"url":{"0":"https:\\/\\/example.com"}}'

In [21]: data2 = json.loads(tmp)

In [22]: data2
Out[22]: {'id': {'0': 'alice'}, 'url': {'0': 'https://example.com'}}

```

# pandasの話
スタックオーバーフローの回答によると、`pandas.DataFrame.to_json`では[usjon](https://github.com/ultrajson/ultrajson)というライブラリが使われているそうです。

https://stackoverflow.com/a/43413418/9156371

`pandas.DataFrame.to_json()`の中身を調べていくと、`pandas.is.json._json.Writer.write`関数で、`pandas._libs.json.ujson_dumps`という関数を呼び出していることが分かりました。

https://github.com/pandas-dev/pandas/blob/d9cdd2ee5a58015ef6f4d15c7226110c9aab8140/pandas/io/json/_json.py#L263

なお、`ujson`はデフォルトではスラッシュをエスケープして出力します。

https://github.com/ultrajson/ultrajson?tab=readme-ov-file#escape_forward_slashes

# スラッシュをエスケープしないようにする方法

`pandas.DataFrame.to_json()`を使わずに`json.dumps`を使えば、スラッシュをエスケープせずにJSON出力できます。

```
In [34]: tmp2 = df.to_dict("records")

In [35]: tmp2
Out[35]: [{'id': 'alice', 'url': 'https://example.com'}]

In [36]: json.dumps(tmp2)
Out[36]: '[{"id": "alice", "url": "https://example.com"}]'

```

ただし、以下の点に注意してください。

## `NaN`はnullに変換されずにそのまま`NaN`が出力される
`pandas.DataFrame.to_json()`は`NaN`を`null`に変換します。

>Note NaN’s and None will be converted to null and datetime objects will be converted to UNIX timestamps.

`pandas.DataFrame.to_json`を使わずに、`pandas.DataFrame.to_dict`で変換して`json.dumps`でJSONを出力すると、`NaN`はそのまま`NaN`と出力されます。

```
In [38]: df = pd.DataFrame({"value": [1.1, None]})

In [39]: df
Out[39]:
   value
0    1.1
1    NaN

In [41]: tmp2 = df.to_dict("records")

In [42]: tmp2
Out[42]: [{'value': 1.1}, {'value': nan}]

In [43]: json.dumps(tmp2)
Out[43]: '[{"value": 1.1}, {"value": NaN}]'
```

`NaN`はJSONの仕様ではありませんが、Pythonの`json`モジュールは`NaN`に対応しています。

```
In [52]: json.loads("NaN")
Out[52]: nan
```

https://zenn.dev/mod_poppo/articles/json-and-extension

### `pandas.DataFrame.to_dict`は`pandas.NA`を`None`に変換する
`pandas.DataFrame.to_dict`は`pandas.NA`を`None`に変換します。
したがって、`pandas.DataFrame.to_json`を使わない場合は、DataFrameの列の型を`float64`ではなくnullableな`Float64`にした方がよいでしょう。

```
In [55]: df = pd.DataFrame({"value": [1.1, None]}, dtype="Float64")

In [56]: df
Out[56]:
   value
0    1.1
1   <NA>

In [57]: tmp3 = df.to_dict("records")

In [58]: tmp3
Out[58]: [{'value': 1.1}, {'value': None}]
```

# まとめ
* `pandas.DataFrame.to_json`はスラッシュをエスケープして出力する
* JSONの仕様では、スラッシュをエスケープして出力することは必須でない
* `pandas.DataFrame.to_json`は`ujson`ライブラリを使ってJSONを出力している
    * `ujson`はデフォルトでは、スラッシュをエスケープして出力する
* `pandas.DataFrame.to_json`は`NaN`を`null`に変換して出力する



# 参考にしたサイト
https://shinkufencer.hateblo.jp/entry/2018/08/03/235900