json-pyのラッパを書いてみた

json-py が意外に使いにくかったので, Pythonの普通のオブジェクトと, JSON文字列の間で直接変換できるように, ちょっとラッパを書いてみた. なんか, いかにも誰かが既に書いていそうなものだけど, まあ, 勉強ということで. json2と命名. 安易だ.

ラッパの設計上問題になったのは, JSON文字列からオブジェクトを構成する際に, どうやってオブジェクトの方情報を渡すか. 先日のエントリでは, テンプレートになるオブジェクトを渡してフィルしてもらう方針で疑似コードを書いてみたのだが, これだとネストしたオブジェクトの取り扱いが難しそうなので断念. クラス変数で型宣言する方法にしてみた.

構造

  • 型の宣言には, str, int, float, long, および 他のクラス名, これらを含んだリスト, ディクショナリが使用できる.
  • JSON文字列から変換する場合には, 第二引数で型を指定する.
  • 変換対象のオブジェクトは, 引数なしでインスタンス生成できなければならない. これは, ラッパのなかでインスタンス生成する際に, 引数なしでコンストラクタを呼び出すため.
  • json-pyに対するラッパなので, やったのは, オブジェクトとディクショナリ構造の間での変換だけ. そこから先はjson-pyにお任せ.

使い方

宣言付きのオブジェクトはこんな感じ. このクラスは, ホスト名とポートからなる host というクラスを定義している. __repr__は, もちろん, なくてもいい.

class host(object):
    hostname = str
    port = int
        
    def __init__(self, _hostname = None, _port = None):
        self.hostname = _hostname
        self.port = _port

    def __repr__(self):
        return "host( hostname = %s, port = %d)" % (self.hostname, self.port)

これをJSONに変換するとこんな感じ.

>>> import json2
>>> json2.write(host('localhost', 8080))
'{"hostname":"localhost","port":8080}'
    
JSON文字列を読む時には型を指定する
>>> json2.read('{"hostname":"localhost","port":8080}', host)
host( hostname = localhost, port = 8080)
ネストしたオブジェクトも処理できる.
class hostGroup(object):
    name = str
    hosts = [host]

    def __init__(self, _name = None, _hosts = []):
        self.name = _name
        self.hosts = _hosts

    def __repr__(self):
        return "%s: [%s]" % (self.name, ",".join(str(host) for host in self.hosts))
このクラスは hostのリストをhostsフィールドに持つ. このクラスをJSON文字列に変換.
>>> json2.write(hostGroup("groupA", [host("localhost", 8000), host("example.com", 80)]))
'{"hosts":[{"hostname":"localhost","port":8000},{"hostname":"example.com","port":80}],"name":"groupA"}'
>>> 
戻すときには型名を指定.
>>> json2.read("""{"hosts":[
...             {"hostname":"localhost","port":8000},
...             {"hostname":"example.com","port":80}],
...     "name":"groupA"}"""
... , hostGroup)
groupA: [host( hostname = localhost, port = 8000),host( hostname = example.com, port = 80)]
>>> 

ソース

自分が使う範囲でしか動作確認していません. エラーハンドルも適当. json2.py というファイル名でセーブすれば, 上のサンプルは動くはず.
import json
import logging

"""
This module helps to transform python object to/from
json string. 

It assumes that python objects declares its fields
type as class fields, like django models.
It also assumes that the instance can be created
without arguments; i.e. __init__ allows no argument
instance creation.

"""

def write(o):
    """ to json string """
    return json.write(_toPlain(o))

def read(s, t):
    return _fromPlain(json.read(s), t)

def _toPlain(o):
    if isinstance(o, str) or isinstance(o, int) \
    or isinstance(o, long) or isinstance(o, float) \
    or isinstance(o, bool) or o == None:
        return o
    if isinstance(o, list):
        res = 
        for item in o:
            res.append(_toPlain(item))
        return res
    if isinstance(o, dict):
        res = {}
        for key in o.keys():
            res[key] = _toPlain(o[key])
        return res
    # looks like object
    res = {}
    for key in o.__class__.__dict__:
        if not key.startswith("__") and o.__dict__.has_key(key):
            res[key] = _toPlain(o.__dict__[key])
    return res

def _fromPlain(p, t):
    if p == None:
        return p
    if t == str or t == int or t == long \
    or t == float or t == bool:
        if isinstance(p, t):
           return p
        logging.error("type mismatch: %s is '%s', while type specified was %s",
                      (str(p), str(type(p)), str(t)))
        return p
    if isinstance(p, list) and isinstance(t, list):
        res = 
        for i in range(len(p)):
            item = p[i]
            res.append(_fromPlain(item, t[i % len(t)]))
        return res
    if isinstance(p, dict):
        if isinstance(t, dict):
            res = {}
            for key in p.keys:
                res[key] = _fromPlain(p[key], t.values[0])
            return res
        else:
            # to object 
            res = t()
            for key in t.__dict__:
                if not key.startswith("__"):
                    res.__dict__[key] = _fromPlain(p[key], t.__dict__[key])
            return res
    else:
        logging.error("cannot handle")
  • 7月1日修正. Noneとboolに対応
  • 7月2日修正. ディクショナリ関連のバグ修正.