Python - json, json5 json パーサ - is not JSON serializable

 クラウディア
1. 概要
2. datetime
3. いわゆるオブジェクト

1. 概要

 表題に書いてある、エラーには、「python」の「json」モジュールを使うと、必ずぶつかる問題と言ってもいいかと思います。  一度は、ぶつかって、わたしの場合、その後も何度も悩まされている問題です。  本ページは、下記のサイトを参考にさせていただきました。
【Python】 json.dumpsのエラー対処: object of type is not json serializable」
「Pythonで型を取得・判定するtype関数, isinstance関数

2. datetime


import datetime
import json

data = dict()
data['members'] = [
  { 'name': 'ほげほげ', 'number': 1, 'datetime': datetime.datetime.now()},
  { 'name': 'ふがふが', 'number': 2, 'datetime': datetime.datetime.now() },
]

with open('json02.json', mode='wt', encoding='utf-8') as file:
  json.dump(data, file, ensure_ascii=False, indent=2)
 てなソースを書いて、実行するとこんなことになっちゃいます。

Traceback (most recent call last):
  File "ソースファイル名.py", line 13, in <module>
    json.dump(data, file, ensure_ascii=False, indent=2)
  File "/パス/json/__init__.py", line 179, in dump
    for chunk in iterable:
  File "/パス/json/encoder.py", line 431, in _iterencode
    yield from _iterencode_dict(o, _current_indent_level)
  File "/パス/json/encoder.py", line 405, in _iterencode_dict
    yield from chunks
  File "/パス/json/encoder.py", line 325, in _iterencode_list
    yield from chunks
  File "/パス/json/encoder.py", line 405, in _iterencode_dict
    yield from chunks
  File "/パス/json/encoder.py", line 438, in _iterencode
    o = _default(o)
  File "/パス/json/encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type datetime is not JSON serializable
 要は、「『datetime』型は、シリアライズできまへんがな」ちゅうことですな。  これも参考サイトを元に、シリアライズできない型は、シリアライズ可能な方へ変換。

from datetime import datetime
import json

def support_datetime_default(o):
    if isinstance(o, datetime):
        return o.isoformat()
    raise TypeError(repr(o) + " is not JSON serializable")

data = dict()
data['members'] = [
  { 'name': 'ほげほげ', 'number': 1, 'datetime': datetime.now() },
  { 'name': 'ふがふが', 'number': 2, 'datetime': datetime.now() },
]

with open('json03.json', mode='wt', encoding='utf-8') as file:
  json.dump(data, file, ensure_ascii=False, indent=2, default=support_datetime_default)
 てなソースを書きます。  4~7行が、コールバック関数で、「json.dump」の際に、それを引数として与えています。  実行すると下記の内容のファイルが出力されます。

{
  "members": [
    {
      "name": "ほげほげ",
      "number": 1,
      "datetime": "2021-06-25T13:55:38.313119"
    },
    {
      "name": "ふがふが",
      "number": 2,
      "datetime": "2021-06-25T13:55:38.313124"
    }
  ]
}
 あるいは、

from datetime import datetime
import json

class DateTimeSupportJSONEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, datetime):
            return o.isoformat()
        return super(DateTimeSupportJSONEncoder, self).default(o)

data = dict()
data['members'] = [
  { 'name': 'ほげほげ', 'number': 1, 'update': datetime.now()},
  { 'name': 'ふがふが', 'number': 2, 'update': datetime.now() },
]

with open('json04.json', mode='wt', encoding='utf-8') as file:
  json.dump(data, file, ensure_ascii=False, indent=2, cls=DateTimeSupportJSONEncoder)
 てなソースを書きます。  これは、4~8行が、「JSONEncoder を継承したクラス」だそうで、そのクラスを使うように引数で指定しているのですな。  実行するとさっきと同様のファイルが出力されます。

3. いわゆるオブジェクト

 前項に限らず、いわゆるスカラ変数以外のもの。  オブジェクトというのかしら。  てなものを出力しようとすると、基本、シリアライズできまへんがな・・・。  と言われます。

class Member:

  ## Member::__init__
  # @brief  初期化関数
  # @return 戻り値なし
  def __init__(self, uri):

    self.uri = uri                                                              # URI
    self.status   = False                                                       # 取得済
    self.error    = False                                                       # エラー
    self.title    = None                                                        # タイトル
    self.document = None                                                        # ドキュメント
    self.modified = None                                                        # 最終懇親日時
    self.description = None                                                     # description  当面はタイトルを適用
    self.body     = None                                                        # body

    ## リンク 先の配列
    self.links = []
 上記は、わたしが実際に使っている、オブジェクトなのですが。  こいつがはいっている、配列をダンプしようとすると(「tree」という変数が、対象の配列)。

with open('../contents.json', mode='wt', encoding='utf-8') as file:
  json.dump(tree, file, ensure_ascii=False, indent=2)
 下記のようなことになる(一部略)。

  File "/usr/local/lib/python3.9/json/__init__.py", line 179, in dump
    for chunk in iterable:
  File "/usr/local/lib/python3.9/json/encoder.py", line 429, in _iterencode
    yield from _iterencode_list(o, _current_indent_level)
  File "/usr/local/lib/python3.9/json/encoder.py", line 325, in _iterencode_list
    yield from chunks
  File "/usr/local/lib/python3.9/json/encoder.py", line 438, in _iterencode
    o = _default(o)
  File "/usr/local/lib/python3.9/json/encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type Member is not JSON serializable
 こういう場合は、オブジェクトに「__dict__」ていう特殊属性なるものをのをつけてやれば。  辞書型のように解釈してダンプしてくれるようです。  すなわち。

class MemberSupportJSONEncoder(json.JSONEncoder):
  def default(self, o):
    if isinstance(o, Member.Member):
      return o.__dict__
    return super(MemberSupportJSONEncoder, self).default(o)

with open('../contents.json', mode='wt', encoding='utf-8') as file:
  json.dump(tree, file, ensure_ascii=False, indent=2, cls=MemberSupportJSONEncoder)
 と書いてやれば、うまくダンプできるのでした。  「class MemberSupportJSONEncode」の記述で、インスタンスが「Member.Member」でないときの処理を書いていないのが不十分なのですが。  今んとこ(2023年10月18日)、これで動いているので、これでいいのだ。  これ、本当は、「json5」で書きたいのですが・・・。  「json5」のモジュールには、「JSONEncoder」もしくは「JSON5Encoder」というものがないと言われるのだ。  逃げ道で、上記に続けて。

with open('../contents.json', mode='r', encoding='utf-8') as file:
  load = json5.load(file)

with open('../contents.json', mode='wt', encoding='utf-8') as file:
  json5.dump(load, file, ensure_ascii=False, indent=2)
 と書くことで、最終的なファイルを「json5」形式にしました。
earthcar(アースカー)
U-NEXT
葬送のフリーレン Prime Video
JETBOY