背景
- 内部システム向けにWebAPIを作ろうということになったが、一部のメンバーがRESTに嫌気が指していた。曰く、「(完全な)RESTに則るのは費用対効果に合わない。」
- 彼の発言を拡大解釈した結果、REST以外の思想に乗っ取りAPIを作りたいのだと思いを汲み取り、名前からしてパット見は全くRESTじゃなさそうなJSON-RPCというデータフォーマットの規格について調べた
JSONのデータフォーマット
JSONそれそのものの規格はRFC 7159で決まっていますが、それに則りその上でどういった要素にデータを格納していくべきかの設計は各人の好みが別れる所。
例えば、下記で言うとメタ情報はheader
、データ部はbody
のリスト、リクエストIDはid
、時刻はtimestamp
、何かしら問題があればerror
といった、どういう名称の要素に、どういったkey-valueで格納するかなど実運用上、決定しなければならないことが多い。決めの問題といえばその通りだけど、往々にして時間がかかるこの手の問題はなるべく避けてたいときも多いだろう。
{
"header": {/*何かしらのメタ情報*/},
"body" : [{/*データ部1*/}, {/*データ部2*/}, {/*データ部3*/}],
"id" : "処理ID",
"timestamp" : "yyyyMMddTHHmmssSSSxxxx"
"error" : {"code": "59231", "message":"parse error"} # エラーはレスポンスの時だけ
}
ということで、先人たちが設計したJSONフォーマットを調べることは非常に価値があると思う。
まさに巨人の肩に乗るということだ。
ちなみに、JSONにハイパーメディアな要素を加えたHATEOASというパラダイムもあり、その効果もありJSONのデータフォーマットの規格は乱立している状態だったりもする。
機会があれば紹介していきたい。(思いの外たくさんありとても驚いた)
調べた結果のサマリ
- JSON-RPCはREST思想ではないRPCの規格、それゆえHTTPのWebAPIとして導入すると、利用者に覚えさせることが多くなる
- データの参照であってもHTTP-GETが非推奨、POST推奨なのでちょっとした確認でもRESTツール必須になるという思い切りの良さ、にRESTに慣れた方は戸惑う、と思う
- JSONの中身にメソッド名と引数を指定するということで、シグネチャをドキュメント化する必要がありそうだし、メソッド名設計がまた大変そうということ
- WebAPIとしてはベストプラクティスが整いつつあるRESTに則った方がやはりラク
- もちろん、RPC実装する時は参考にしたら良いと思うよ(golang界隈ではゴールーチン・プロセス間通信でよく使うらしい)
…以下、細かく見ていきます。
JSON-RPC
- Current Version: 2.0
- Created: 2010-03-26
- Updated: 2013-01-04
- http://www.jsonrpc.org/specification
RPCと名前についているとおり、RESTでなはなくRPCとしての利用を想定している規格です。
概略をまとめると…
- ストートレスで軽量なRPCプロトコル
- 同一プロセス上・ソケット上・HTTP上など様々なトランスポート層での利用を想定
- データ形式としてJSON(RFC 4627)を使用
- シンプルに設計されているとのこと
とのこと。
個人的には次のポイントが独自色を感じて気になりました。
- RPCと呼ぶだけあって、リクエストにmethod名を指定、レスポンスにresult で値を返すというのが基本形
- Notification(通知)という機能があり、値を返却しないモードもある
- バッチ 呼び出しの仕様がある
JSON-RPCのサンプル
--> サーバへのデータ送信
<-- クライアントへのデータ送信
最もシンプルなケース。jsonrpcのversionは必須。
戻り値がある場合はresult項目が必須。
--> {"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}
次に、戻り値がないNotificationの場合。
送りっぱなしです。
--> {"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]}
<-- # 何も返さない
--> {"jsonrpc": "2.0", "method": "foobar"}
<-- # 何も返さない
次はバッチという複数の要求をまとめて送信する例。
methodが異なってもOK、というか基本的に異なるメソッドをまとめて送ることを想定しているようでした。同じメソッドを何回も呼びたい場合は場合は引数で工夫すれば対応できそうだからかもしれません。
--> [
{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
{"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
{"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"},
{"foo": "boo"},
]
<-- [
{"jsonrpc": "2.0", "result": 7, "id": "1"},
{"jsonrpc": "2.0", "result": 19, "id": "2"},
{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
]
エラーコード設計。XML-RPCを参考に作られたためほぼ同じとのこと。エラーコードの体系っていつもどう設計したらよいか悩むので良いですね。でもなんでマイナスをつけるんですかね?-32000台を利用するかも謎です。
コード設計は数が少ない分、シンプルでわかりやすいです。
- JSONパースにすら失敗→-32700
- JSONではあるけど、versionやmethodなどの必須な要素が無い→-32600
- 指定されたメソッドが不可→-32601
- 引数がおかしい→32602
- 処理中にエラーが発生→32603
-32000から-32099は独自エラーを設定できる領域だと。
code message meaning -32700 Parse error Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text. -32600 Invalid Request The JSON sent is not a valid Request object. -32601 Method not found The method does not exist / is not available. -32602 Invalid params Invalid method parameter(s). -32603 Internal error Internal JSON-RPC error. -32000 to -32099 Server error Reserved fo r implementation-defined server-errors.
エラー時の例は以下の感じ。
errorというオブジェクトにcodeとmessageを付与する感じ。
--> {"jsonrpc": "2.0", "method": "foobar", "id": "1"}
<-- {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"}
JSON-RPC over HTTP
JSON-RPCのversion2.0でHTTPをトランスポートとして利用する時の規定は無いかと探した1結果、ここを見つけました。
これが公式もしくはデファクト仕様だと信じて中身を覗いてみました。
それによると以下のように規定されていました。
まずはPOSTからです。
- POSTリクエストの場合
- Content-Typeは application/json
- Content-Lengthは必須
- Acceptは application/json
…普通ですね。続いてGETです。
- GETリクエストの場合
- 意外でもなんでもないかもしれませんが、 非推奨 とのこと。
- URLエンコーディンの必要性やら、JSONのようなネストした構造をGETパラメータに指定するには適さないのが理由みたいです。RESTの概念から入るとオヤって思いますが、理由を聞けば納得しますね。RPCだと。
- もし、GETでアクセスしたい場合は、JSON-RPCをラップするREST-APIを作成することが推奨とのことです。なるほど。
- 意外でもなんでもないかもしれませんが、 非推奨 とのこと。
…RPCベースの考え方だとごもっともだと思いました。
- レスポンス
- Content-Typeはapplication/json.
- Content-Lengthは必須
- Acceptはaplication/json.
…普通ですね。
HTTPステータスコードも基本的にほぼ200で返し、異常時はJSONのerror項目で表現するのがベースみたいです。ただ、一部のケースはHTTPステータスコードでエラーを表現するようです。
HTTP/1.1 200 OK
Connection: close
Content-Length: ...
Content-Type: application/json
Date: Sat, 08 Jul 2006 12:04:08 GMT
{"jsonrpc": "2.0", "id": 6,
"error": {"code": -32600, "message": "Invalid Request.", "data": "'method' is missing"}
}
一覧です。
Notificationの場合に204(または202)を返すのは良いですね。
コード メッセージ 詳細 200 OK レスポンス(Errorも含む) 202 No Response 空レスポンス(Notificationのレスポンスなど) 204 Accepted 空レスポンス(Notificationのレスポンスなど) 307 Temporary Redirect HTTPリダイレクションの場合(リクエストが自動的に再送信されないことがあります) 308 Permanent Redirect HTTPリダイレクションの場合(リクエストが自動的に再送信されないことがあります) 405 Method Not Allowed GETをサポートしていない時はこれ。サポートしていても安全じゃないorべき等じゃ無いメソッドコールの場合はこれ。 415 Unsupported Media Type Content-Typeがapplication/jsonじゃない場合。 その他 - JSON-RPCのスコープ外のHTTPエラー
仕様まとめ
- ほぼデータフォーマットのみを規定した仕様なため、確かに覚えることは少なく軽量だというのはその通りという印象。
- しかし、versionだとかは省略したかったり、idが無いと通知に扱われたりは個人的には趣味に合わないところもある(設計の意図を聞いたら納得しそうではあるけど)
- JSON-RPCという名前の通りあくまでRPCとしての利用を想定している。HTTP以外での使い勝手がどうなのかは気になった。
- HTTPでは、GETが非推奨、基本的にPOSTで利用するサービスをMethod名で指定するスタイル
- エラーコード体系などは参考になる(というかこれだけ流用したい)
- 仕様じゃないですが、goの標準実装で使えますね
-
version1.0のwikiに数行ですが説明がありましたが、2.0のサイトには見当たらなかったです ↩