JSONをURIに埋め込んでも%地獄にならない「Rison」のススメ

さくらのクラウドでバックエンドを担当しております、@townewgokgok と申します（記事はフロントエンド寄りの記事になります）。これは [さくらインターネット Advent Calendar 2018](https://qiita.com/advent-calendar/2018/sakura) 11日目の記事です。

# JSONのように階層化された値をURLに埋め込みたいことってありませんか？

たとえば [価格.com の商品検索結果ページ](http://kakaku.com/pc/ssd/itemlist.aspx?pdf_Spec101=4&pdf_Spec104=3&pdf_Spec301=512-960,960-2000&pdf_so=p1&pdf_ra=3.00) のように、リンクを開いたら検索フォームの内容が復元されて、URLのコピー時に見ていたものがそのまま表示されて欲しい。

これを実現するには、従来なら文字列のキーバリューとしてごく一般的な application/x-www-form-urlencoded 形式でURLにパラメータを埋め込むところです（上記の価格.comの例でもやはりそうなっています）。ただ、そこそこ複雑な検索フォームの値をいちいちこの形式にまとめたり復元したりするのはわりと面倒ですし、配列や真偽値の扱い方が定まっていない点で迷います。

このような場合、クエリパラメータにJSONをそのまま埋め込むととても簡単そうです。モダンなフロントエンド開発手法でフォームの入力内容が自動的にモデル化されているなら、その値を単にJSON化してそのままURI中に保存・復元すれば良いわけです。

# 試してみましょう

ということで、 `{"name":"foo","selected":[1,2,3],"flags":{"a":true,"b":false}}` というJSONをそのままURIエンコードしてクエリパラメータに埋め込んでみます。

- https://townewgokgok.github.io/qschecker/?query=%7B%22name%22%3A%22foo%22%2C%22selected%22%3A%5B1%2C2%2C3%5D%2C%22flags%22%3A%7B%22a%22%3Atrue%2C%22b%22%3Afalse%7D%7D

リンク先は適当に作った確認用ツールです。実際に開いてみると、確かに正しくデコードされることが分かると思います。

まぁこれでも一応は目的を果たせそうですが、どうもスッキリしません。

- 無駄に長い（JSONで多用される `"` がすべて `%22` になるため、それだけでかなり冗長）
- `%` だらけで、一見何をするURIなのかが分からない
- JSONの仕様のためエスケープした文字がある場合、それがさらにパーセントエンコードされてカオス

もうちょっといい方法ないの？ ということで、**Rison** というフォーマットをご紹介します。

# Rison

[Rison](https://rison.io/) は、JSONとほぼ互換のデータ交換フォーマットです。パーセントエンコードの必要がない記号を用いるため、URIに埋め込んでも `%` だらけになりません。

先ほどのクエリパラメータ中のJSONを Rison（正確には後述の O-Rison）で書き直すと、以下のようになります。

- **Before:**
  https://townewgokgok.github.io/qschecker/?query=%7B%22name%22%3A%22foo%22%2C%22selected%22%3A%5B1%2C2%2C3%5D%2C%22flags%22%3A%7B%22a%22%3Atrue%2C%22b%22%3Afalse%7D%7D

- **After:**
  https://townewgokgok.github.io/qschecker/?query=name:foo,selected:!(1,2,3),flags:(a:!t,b:!f)

素晴らしくスッキリしました。Kibana でも採用されているので、何となく見覚えがあるという方も少なくないと思います。

ハッシュでも同じことをやってみましょう。

- **Before:**
 https://townewgokgok.github.io/qschecker/#%7B%22name%22%3A%22foo%22%2C%22selected%22%3A%5B1%2C2%2C3%5D%2C%22flags%22%3A%7B%22a%22%3Atrue%2C%22b%22%3Afalse%7D%7D

- **After:**
  https://townewgokgok.github.io/qschecker/#name:foo,selected:!(1,2,3),flags:(a:!t,b:!f)

フォームの入力内容とハッシュが連動するようなSPAなどでは、その対応関係が一目瞭然になって良さげですね。

# RisonとJSONの比較

それぞれのフォーマットで用いられている特殊記号を比較すると、以下のようになります。

|Rison   |JSON   |意味          |
|:-------|:------|:-------------|
|`(`…`)` |`{`…`}`|オブジェクト ※1 |
|`!(`…`)`|`[`…`]`|配列 ※1       |
|`'`…`'` |`"`…`"`|文字列 ※2     |
|`!!`    |       |文字列中の `!` ※3|
|`!'`    |       |文字列中の `'` ※3|
|`!t`    |`true` |true          |
|`!f`    |`false`|false         |
|`!n`    |`null` |null          |

**※1** オブジェクトや配列中での `:` `,` の使い方については、どちらも変わりません。

**※2** 以下の文字だけで構成される文字列やオブジェクトキーは、`'` で括る必要がありません。ただし、数字やハイフンで始まる文字列を括らずに書いた場合は string ではなく number として解釈されます。

- 半角英数字
- `-` `_` `.` `/` `~`
- 非ASCIIなUnicode文字（日本語等）

**※3** JSONの `\` を用いたエスケープ文字（`\uXXXX` や `\xXX` も含む）に当たるものは Rison にはありません。`!!` `!'` 以外は常に生の文字を書く必要があります。ということは、たとえば改行もそのまま改行として記述することになるわけですが、URIに埋め込む分には結局パーセントエンコードするので1行で表されます。

なお、数値の指数表現は `2.998e8` `1.6e-35` のように書く必要があります。大文字の `E` を使用したり `+` を明示することはできません。

[Rison Playground](https://rison.io/) にて Rison と JSON の相互変換を試すことができるので、実際にやってみると理解しやすいと思います。

# O-Rison と A-Rison

O-Risonとは、オブジェクトを表す Rison `(a:A,b:B,c:C)` のトップレベルの括弧を略して `a:A,b:B,c:C` と表記するバリエーションのことです。A-Rison は同様に配列の括弧を略したものです。

ここまで見てきた `?query=...` に埋め込む値の例はすべて O-Rison で記述しており、トップレベルの括弧が省略されています。今回のようにキーバリューの代替として使用するケースでは、トップレベルは必ずオブジェクトになるので、O-Rison が適しています。

# %地獄にならないワケ

Rison をパーセントエンコードする際は最低限の文字だけを対象とするよう、Rison ライブラリには [専用のエンコーダ](https://github.com/Nanonid/rison/blob/e64af6c096fd30950ec32cfd48526ca6ee21649d/js/rison.js#L98-L118) が付属しています。例えば、半角記号では `~` `!` `*` `(` `)` `-` `_` `.` `,` `:` `@` `$` `'` `/` をパーセントエンコードしません（一般的なデコーダは、これらの文字が生で含まれているURIを正しくデコードできます）。Rison が用いる特殊記号はすべてこれらの文字セットに含まれているため、文字列リテラルに `?` `=` `#` 等の記号が含まれない限りはパーセントエンコードが不要となります。

ちなみに、専用エンコーダは半角スペースを `%20` ではなく `+` にエンコードします。これを含む値をクエリパラメータに埋め込んだ場合は通常 `+` から半角スペースにデコードされるので問題ありませんが、JavaScript の `decodeURIComponent` などは `+` を半角スペースにデコードしません。ハッシュに埋め込んだRisonを自前でデコードする場合などは注意が必要です。

参考までに、`encodeURIComponent` と `rison.quote` を比較するコードを [貼っておきます](https://jsfiddle.net/townewgokgok/hn8b5cyt/)。

# 以上です

いかがでしたでしょうか。さくらのクラウドでは、社内設置の運用関連ツールでRisonを使用しています。各言語・フレームワーク用のライブラリも多数見つかりますので、[Githubでrisonを検索](https://github.com/search?q=rison) してみてください。ちなみに、私が作成した [Go版](https://github.com/sakura-internet/go-rison) も公開しております。