json – JavaScript Object Notation シリアライザ

目的:Python オブジェクトを JSON 文字列へエンコード、JSON 文字列を Python オブジェクトへデコードする
利用できるバージョン:2.6

json モジュールはインメモリの Python オブジェクトを JavaScript Object Notation (JSON) というシリアライズされたフォーマットへ変換する pickle に似た API を提供します。pickle と違うのは、JSON はアプリケーション間通信に適応するように多くの言語(特に JavaScript)で実装されている利点があります。JSON は AJAX アプリケーションにおける web サーバとクライアント間の通信でおそらく最もよく利用されていますが、その課題領域のみに制限されているわけではありません。

シンプルなデータ型をエンコード、デコードする

エンコーダはデフォルトで Python のネイティブのデータ型(string, unicode, int, float, list, tuple, dict)を理解します。

import json

data = [ { 'a':'A', 'b':(2, 4), 'c':3.0 } ]
print 'DATA:', repr(data)

data_string = json.dumps(data)
print 'JSON:', data_string

値は Python の repr() の出力とよく似た値にエンコードされます。

$ python json_simple_types.py
DATA: [{'a': 'A', 'c': 3.0, 'b': (2, 4)}]
JSON: [{"a": "A", "c": 3.0, "b": [2, 4]}]

エンコードした後に再度デコードすると、厳密には同じ型のオブジェクトを取得できないこともあります。

import json

data = [ { 'a':'A', 'b':(2, 4), 'c':3.0 } ]
data_string = json.dumps(data)
print 'ENCODED:', data_string

decoded = json.loads(data_string)
print 'DECODED:', decoded

print 'ORIGINAL:', type(data[0]['b'])
print 'DECODED :', type(decoded[0]['b'])

特に、文字列はユニコードへ、タプルは JSON リストへ変換されます。

$ python json_simple_types_decode.py
ENCODED: [{"a": "A", "c": 3.0, "b": [2, 4]}]
DECODED: [{u'a': u'A', u'c': 3.0, u'b': [2, 4]}]
ORIGINAL: <type 'tuple'>
DECODED : <type 'list'>

人間が読み易い 対 コンパクトアウトプット

pickle より優れている JSON の他の利点はその出力結果を人間が読み易いことです。 dumps() 関数はその出力を読み易くするために複数の引数を受け取ります。例えば、 sort_keys はランダムな順序ではなく、ディクショナリのキーをソートして出力するようにエンコーダへ伝えます。

import json

data = [ { 'a':'A', 'b':(2, 4), 'c':3.0 } ]
print 'DATA:', repr(data)

unsorted = json.dumps(data)
print 'JSON:', json.dumps(data)
print 'SORT:', json.dumps(data, sort_keys=True)

first = json.dumps(data, sort_keys=True)
second = json.dumps(data, sort_keys=True)

print 'UNSORTED MATCH:', unsorted == first
print 'SORTED MATCH  :', first == second

ソートすることで、その出力結果を目視で確認するのに便利です。さらにテストのときに JSON の出力を比較するのも簡単です。

$ python json_sort_keys.py
DATA: [{'a': 'A', 'c': 3.0, 'b': (2, 4)}]
JSON: [{"a": "A", "c": 3.0, "b": [2, 4]}]
SORT: [{"a": "A", "b": [2, 4], "c": 3.0}]
UNSORTED MATCH: False
SORTED MATCH  : True

深くネストされたデータ構造の出力を分かり易くするために indent に値を指定したくなるでしょう。

import json

data = [ { 'a':'A', 'b':(2, 4), 'c':3.0 } ]
print 'DATA:', repr(data)

print 'NORMAL:', json.dumps(data, sort_keys=True)
print 'INDENT:', json.dumps(data, sort_keys=True, indent=2)

インデントが正の数の場合、データ構造の各レベルに対応するインデントレベルにスペースを入れて pprint によく似た出力になります。

$ python json_indent.py
DATA: [{'a': 'A', 'c': 3.0, 'b': (2, 4)}]
NORMAL: [{"a": "A", "b": [2, 4], "c": 3.0}]
INDENT: [
  {
    "a": "A",
    "b": [
      2,
      4
    ],
    "c": 3.0
  }
]

同じデータ量を送信する必要ある冗長な出力には、サンプルのようにバイト数が増加しますが、本番環境で意図したようにソートされるとは限りません。実際、デフォルト設定よりもっとコンパクトになるようにエンコードしたとしても、エンコードされた出力のデータを分割するために設定を調整したくなるかもしれません。

import json

data = [ { 'a':'A', 'b':(2, 4), 'c':3.0 } ]
print 'DATA:', repr(data)
print 'repr(data)             :', len(repr(data))
print 'dumps(data)            :', len(json.dumps(data))
print 'dumps(data, indent=2)  :', len(json.dumps(data, indent=2))
print 'dumps(data, separators):', len(json.dumps(data, separators=(',',':')))

dumps() への separators 引数はディクショナリの値からリストやキーのアイテムを分離するためにその文字列を含むタプルにします。デフォルトは (', ', ': ') です。スペースを削除することでもっとコンパクトな出力になります。

$ python json_compact_encoding.py
DATA: [{'a': 'A', 'c': 3.0, 'b': (2, 4)}]
repr(data)             : 35
dumps(data)            : 35
dumps(data, indent=2)  : 76
dumps(data, separators): 29

ディクショナリをエンコードする

JSON フォーマットはディクショナリのキーが文字列であることを前提とします。ディクショナリのキーに他のデータ型を使用している場合、そのオブジェクトをエンコードしようとすると ValueError が発生します。その制限を回避する1つのワークアラウンドは skipkeys 引数を使用して非文字列のキーを読み飛ばすことです。

import json

data = [ { 'a':'A', 'b':(2, 4), 'c':3.0, ('d',):'D tuple' } ]

print 'First attempt'
try:
    print json.dumps(data)
except (TypeError, ValueError), err:
    print 'ERROR:', err

print
print 'Second attempt'
print json.dumps(data, skipkeys=True)

例外を発生させずに非文字列のキーは単純に無視されます。

$ python json_skipkeys.py
First attempt
ERROR: keys must be a string

Second attempt
[{"a": "A", "c": 3.0, "b": [2, 4]}]

独自のデータ型で使用する

これまでの全てのサンプルは Python の組み込みデータ型がネイティブに json でサポートされているので、そういった組み込みデータ型を使用していました。Python のデータ型と同様にエンコードできるようにしたい独自のデータ型を定義することは珍しいことではありません。そのために2つの方法があります。

先ずエンコードするためのクラスが必要になります。

class MyObj(object):
    def __init__(self, s):
        self.s = s
    def __repr__(self):
        return '<MyObj(%s)>' % self.s

MyObj インスタンスをエンコードする簡単な方法は不明なデータ型をよく知られたデータ型へ変換する関数を定義することです。自分自身でエンコードする必要はありません。ただ1つのオブジェクトを他のオブジェクトへ変換するのみです。

import json
import json_myobj

obj = json_myobj.MyObj('instance value goes here')

print 'First attempt'
try:
    print json.dumps(obj)
except TypeError, err:
    print 'ERROR:', err

def convert_to_builtin_type(obj):
    print 'default(', repr(obj), ')'
    # Convert objects to a dictionary of their representation
    d = { '__class__':obj.__class__.__name__, 
          '__module__':obj.__module__,
          }
    d.update(obj.__dict__)
    return d

print
print 'With default'
print json.dumps(obj, default=convert_to_builtin_type)

convert_to_builtin_type() の関数内で、あるプログラムが Python モジュールへアクセスする必要がある場合、 json が認識しないクラスのインスタンスはそのオブジェクトを再作成するために十分な情報を持つディクショナリへ変換されます。

$ python json_dump_default.py
First attempt
ERROR: <MyObj(instance value goes here)> is not JSON serializable

With default
default( <MyObj(instance value goes here)> )
{"s": "instance value goes here", "__module__": "json_myobj", "__class__": "MyObj"}

その出力結果をデコードして MyObj インスタンスを作成するには、そのモジュールからクラスをインポートして、そのクラスのインスタンスを作成できるようにデコーダへ関連付ける必要があります。そのため loads() への object_hook 引数を使用します。

object_hook は入ってくるデータストリームからデコードされるディクショナリ毎に呼び出されます。つまり、そのディクショナリから他のオブジェクトのデータ型へ変換する機会を提供してくれます。そのフック関数はそのディクショナリの代わりに受け取るための呼び出しアプリケーションを要求するオブジェクトを返すべきです。

import json

def dict_to_object(d):
    if '__class__' in d:
        class_name = d.pop('__class__')
        module_name = d.pop('__module__')
        module = __import__(module_name)
        print 'MODULE:', module
        class_ = getattr(module, class_name)
        print 'CLASS:', class_
        args = dict( (key.encode('ascii'), value) for key, value in d.items())
        print 'INSTANCE ARGS:', args
        inst = class_(**args)
    else:
        inst = d
    return inst

encoded_object = '[{"s": "instance value goes here", "__module__": "json_myobj", "__class__": "MyObj"}]'

myobj_instance = json.loads(encoded_object, object_hook=dict_to_object)
print myobj_instance

json は文字列の値から unicode オブジェクトへ変換されます。そのため、そのクラスのコンストラクタへのキーワード引数として使用される前に、unicode オブジェクトを ASCII 文字列として再エンコードする必要があります。

$ python json_load_object_hook.py
MODULE: <module 'json_myobj' from '/Users/dhellmann/Devel/pymotw-ja/t2y/PyMOTW/json/json_myobj.pyc'>
CLASS: <class 'json_myobj.MyObj'>
INSTANCE ARGS: {'s': u'instance value goes here'}
[<MyObj(instance value goes here)>]

よく似たフック関数が組み込み型の整数(parse_int), 浮動小数点数(parse_float) と定数 (parse_constant) のために使用できます。

クラスのエンコーダとデコーダ

既に説明した便利な関数に加えて、 json モジュールはエンコーディングとデコーディングのクラスを提供します。そのクラスを直接使用する場合、拡張 API へアクセスして、それらのクラスの振る舞いをカスタマイズするサブクラスを作成できます。

JSONEncoder はエンコードデータの “チャンク” を生成するために繰り返し利用できるインタフェースを提供します。そして、メモリ内にある完全なデータ構造を再構成せずに簡単にネットワークソケット又はファイルへの書き込みます。

import json

encoder = json.JSONEncoder()
data = [ { 'a':'A', 'b':(2, 4), 'c':3.0 } ]

for part in encoder.iterencode(data):
    print 'PART:', part

ご覧の通り、その出力はサイズの値をベースにしたモノというよりはむしろ論理的なユニット単位で生成されます。

$ python json_encoder_iterable.py
PART: [
PART: {
PART: "a"
PART: :
PART: "A"
PART: ,
PART: "c"
PART: :
PART: 3.0
PART: ,
PART: "b"
PART: :
PART: [2
PART: , 4
PART: ]
PART: }
PART: ]

encode() メソッドは ''.join(encoder.iterencode()) と基本的には等価です。そして、フロント側でその他のエラーを調べます。

任意のオブジェクトをエンコードするために、上述した convert_to_builtin_type() の方法と同様の実装で default() メソッドをオーバーライドできます。

import json
import json_myobj

class MyEncoder(json.JSONEncoder):
    
    def default(self, obj):
        print 'default(', repr(obj), ')'
        # Convert objects to a dictionary of their representation
        d = { '__class__':obj.__class__.__name__, 
              '__module__':obj.__module__,
              }
        d.update(obj.__dict__)
        return d

obj = json_myobj.MyObj('internal data')
print obj
print MyEncoder().encode(obj)

その出力は以前の実装した内容と同じです。

$ python json_encoder_default.py
<MyObj(internal data)>
default( <MyObj(internal data)> )
{"s": "internal data", "__module__": "json_myobj", "__class__": "MyObj"}

テキストをデコードした後、そのディクショナリをオブジェクトへ変換するには、以前の実装より少しだけ作業が必要ですが、大したことはありません。

import json

class MyDecoder(json.JSONDecoder):
    
    def __init__(self):
        json.JSONDecoder.__init__(self, object_hook=self.dict_to_object)

    def dict_to_object(self, d):
        if '__class__' in d:
            class_name = d.pop('__class__')
            module_name = d.pop('__module__')
            module = __import__(module_name)
            print 'MODULE:', module
            class_ = getattr(module, class_name)
            print 'CLASS:', class_
            args = dict( (key.encode('ascii'), value) for key, value in d.items())
            print 'INSTANCE ARGS:', args
            inst = class_(**args)
        else:
            inst = d
        return inst

encoded_object = '[{"s": "instance value goes here", "__module__": "json_myobj", "__class__": "MyObj"}]'

myobj_instance = MyDecoder().decode(encoded_object)
print myobj_instance

その出力は以前の例と同じです。

$ python json_decoder_object_hook.py
MODULE: <module 'json_myobj' from '/Users/dhellmann/Devel/pymotw-ja/t2y/PyMOTW/json/json_myobj.pyc'>
CLASS: <class 'json_myobj.MyObj'>
INSTANCE ARGS: {'s': u'instance value goes here'}
[<MyObj(instance value goes here)>]

ストリームとファイルで使用する

これまでの全ての例では、メモリ内に完全なデータ構造が構成されているときにエンコードデータを対象として扱える(扱うべき)ということを前提としていました。巨大なデータ構造、例えばファイルのようなオブジェクトへ直接的にそのエンコードデータを書き込む方が望ましいでしょう。そのための便利な関数 load()dump() は、読み書き用途にファイルのようなオブジェクトへのリファレンスを受け取ることができます。

import json
import tempfile

data = [ { 'a':'A', 'b':(2, 4), 'c':3.0 } ]

f = tempfile.NamedTemporaryFile(mode='w+')
json.dump(data, f)
f.flush()

print open(f.name, 'r').read()

ソケットもほぼ同様の方法で普通のファイルハンドラとして動作するでしょう。

$ python json_dump_file.py
[{"a": "A", "c": 3.0, "b": [2, 4]}]

一度にデータの一部のみを読み込むように最適化されてはいませんが、 load() 関数は入力ストリームからオブジェクトを生成するカプセル化のロジックの利点も提供します。

import json
import tempfile

f = tempfile.NamedTemporaryFile(mode='w+')
f.write('[{"a": "A", "c": 3.0, "b": [2, 4]}]')
f.flush()
f.seek(0)

print json.load(f)
$ python json_load_file.py
[{u'a': u'A', u'c': 3.0, u'b': [2, 4]}]

混在するデータストリーム

JSONDecoder には終了文字のある JSON データのような、複数のデータで構成されたデータ構造をデコードするために raw_decode() メソッドがあります。その返り値は入力データをデコードすることで作成されるオブジェクトと、そのデータのデコードが終了した位置を指すインデックスです。

import json

decoder = json.JSONDecoder()
def get_decoded_and_remainder(input_data):
    obj, end = decoder.raw_decode(input_data)
    remaining = input_data[end:]
    return (obj, end, remaining)

encoded_object = '[{"a": "A", "c": 3.0, "b": [2, 4]}]'
extra_text = 'This text is not JSON.'

print 'JSON first:'
obj, end, remaining = get_decoded_and_remainder(' '.join([encoded_object, extra_text]))
print 'Object              :', obj
print 'End of parsed input :', end
print 'Remaining text      :', repr(remaining)

print
print 'JSON embedded:'
try:
    obj, end, remaining = get_decoded_and_remainder(
        ' '.join([extra_text, encoded_object, extra_text])
        )
except ValueError, err:
    print 'ERROR:', err

    

不幸にも raw_decode() は終了文字のあるデータ構造のオブジェクトが入力データの最初に現れた場合しか動作しません。

$ python json_mixed_data.py
JSON first:
Object              : [{u'a': u'A', u'c': 3.0, u'b': [2, 4]}]
End of parsed input : 35
Remaining text      : ' This text is not JSON.'

JSON embedded:
ERROR: No JSON object could be decoded

See also

json
本モジュールの標準ライブラリドキュメント
JavaScript Object Notation
JSON のホームページでドキュメントや他言語の実装がある
http://code.google.com/p/simplejson/
Bob Ippolito と他の人による simplejson は Python2.6 と Python 3.0 で提供された json ライブラリの開発バージョンとしてメンテナンスされている、Python 2.4 と 2.5 にも後方互換性があるようにメンテナンスされている
jsonpickle
jsonpickle は任意の Python オブジェクトを JSON にシリアライズする
データの永続化と変換
Python プログラムからデータを格納するその他のサンプル
Bookmark and Share