apistarのJSON response でdatetimeを含んだdictを返しても大丈夫なようにする

apistarというdjango rest frameworkなどを作っている人たちが作っているフレームワークがある。まだ開発中。既存のpythonフレームワークとは異なりapiサーバーの実装の方をデフォルトとしているのでちょっとしたものを作る分には便利。

このapistarでよくあるjson.dumpsできないdictをそのままJSONレスポンスにする方法を調べたのでメモ。

前提

$ pip freeze | grep apistar
apistar==0.5.25

開発が進むとともにこのメモの内容は古くなる可能性がある

defaultではエラーになる

例えばテキトウにdatetime.now()を含んだdictを返すAPIを作ってみる。

from apistar import App, Route
from datetime import datetime

def hello() -> dict:
    return {
        "hello": "world",
        "now": datetime.now(),
    }


if __name__ == "__main__":
    routes = [
        Route("/", "GET", hello),
    ]
    app = App(routes=routes)
    app.serve('127.0.0.1', 5000, debug=True)

エラーになる。

  File "/usr/lib/python3.6/_weakrefset.py", line 75, in __contains__
    return wr in self.data
RecursionError: maximum recursion depth exceeded in comparison

これをどうにかしたい。

現状ではrendering方法をカスタマイズする方法はそんざいしていない

現状ではrendering方法をカスタマイズする方法はそんざいしていない。実際中のコードを見てみると以下のようになっている。

class JSONResponse(Response):
    media_type = 'application/json'
    charset = None
    options = {
        'ensure_ascii': False,
        'allow_nan': False,
        'indent': None,
        'separators': (',', ':'),
    }

    def render(self, content: typing.Any) -> bytes:
        options = {'default': self.default}
        options.update(self.options)
        return json.dumps(content, **options).encode('utf-8')

    def default(self, obj: typing.Any) -> typing.Any:
        if isinstance(obj, types.Type):
            return dict(obj)
        error = "Object of type '%s' is not JSON serializable."
        return TypeError(error % type(obj).__name__)

json.dumpsのdefaultにJSONResponse.defaultが渡されてはいるものの。dict()を試すということしかしていない。そんなわけでまだ対応していないようだった。

work-around

仕方がないので以下の様なコードを書く。ついでにtypingも使っておく。

import typing
from apistar import App, Route
from apistar.http import Response, HTMLResponse, JSONResponse
from apistar.server.components import ReturnValue
from datetime import datetime


class MyApp(App):
    def render_response(self, return_value: ReturnValue) -> Response:
        if isinstance(return_value, Response):
            return return_value
        elif isinstance(return_value, str):
            return HTMLResponse(return_value)
        return MyJSONResponse(return_value)


class MyJSONResponse(JSONResponse):
    def default(self, obj: typing.Any) -> typing.Any:
        if isinstance(obj, datetime):
            return obj.isoformat()
        return super().default(obj)


def hello() -> dict:
    return {
        "hello": "world",
        "now": datetime.now(),
    }


if __name__ == "__main__":
    routes = [
        Route("/", "GET", hello),
    ]
    app = App(routes=routes)
    app.serve('127.0.0.1', 5000, debug=True)

今度は通る。

$ python app.py &
$ http -b :5000/
{
    "hello": "world",
    "now": "2018-05-26T15:02:56.854735"
}

余談

とは言え、色々apistarの機能は開発中で変わることもありうる。ついでに、swagger(openAPI)に対応しようとしたりgraphqlもサポートできたら良いねみたいなissueもあったりする通り、型をどこかで定義してそこからのマッピングをしようという方向に考えているように見える。

実際にtypingを使った型を定義してそちらを使って自動でJSONに変換しようというような例がドキュメントにも存在している。なのでそちらの方向に進んでいくような気もしている。