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に変換しようというような例がドキュメントにも存在している。なのでそちらの方向に進んでいくような気もしている。