今では当たり前のようにあちこちで使われるJSON。
JavaScript Object Notationだったけど、今では完全に汎用フォーマット。JavaScriptでも eval
せずパーサで読み出すべし(セキュリティ上の理由)。厳密な定義はないけれども、様々なプログラミング言語で「コンテナクラス」と呼ばれるものは、おおむねJSONのカバー範囲と同じになる。REST API でのオブジェクトのリソース表現として使うなど、システム間結合でよく使われる。
使用上のTIPSを書き連ねてみる。設計の助けになれば。
ToString
Java ToString()
, Python __str__
, Go String()
(stringer interface) と、オブジェクトを文字列に変換したいケースは、たくさんある。ここに json を使おうというのもよくわかるし、自然だと思う。
ただし、次のようなケースに注意。
import json
class Example(object):
def __init__(self, name):
self.name = name
self.prop1 = "prop1"
def __str__(self):
return json.dumps({"name":self.name, "prop1":self.prop1})
obj = Example("hello")
print(str(obj))
# {"name": "hello", "prop1": "prop1"}
一見イケてる感じがする。が、こうなるとどうだろうか?
print(json.dumps({"hello":Example("hello")}))
# Traceback (most recent call last):
# ...中略...
# TypeError: <__main__.Example object at 0x6ffffd14a58> is not JSON serializable
イケてない。臥竜点睛を欠く。これの解はいくつかある。とりあえずの回避策としては json.dumps(..., default=lambda x:json.loads(str(x)))
とする方法。JSONEncoder
に手を加えるという手もあるけれど、ちゃんとやるなら Example
を json が扱える型の派生型にする。つまりこの場合は dict
の派生型にするのが良い。
型
汎用フォーマット故に、型が足りないこともよくある。基本的には文字列に変換して対応するのが良い。よくあるケース:
- DateTime : 基本的には ISO8601 フォーマットの文字列にするのが吉。timezoneとか登場して一見面倒そうだけれど頑張るべし。どうしてもサイズを小さくしたい場合は Unix epoch time を使うという手もある。
- Binary : 基本的には Base64 にするのが良いだろう。
data://
スキームという形式もある。今時そんなケースはあまりないだろうけど、network より CPU が厳しい場合は、hex 表記にするという手もある。 - Float : Number を直接使っていればほとんど問題は起こらないのだが、ごくごくマレに精度落ちを気にする場合がある。仮数部・指数部を書き出せるエンコーダを使っている場合は、ある程度なんとかなったりする。何ともならない場合は、思い切って文字列表記にしてしまった方が安定する。具体的には例えばpythonでは
"%e" % num
としたりする。
Type hinting
JSON は汎用コンテナなので、クラス情報を直接保存することはできない。そのため下記のように type hinting を追加する場合がある。
-
JSON-RPC に
__jsonclass__
を使った仕様がある。 - Microsoft WCF(Windows Communication Foundation)に
__type
を使った仕様がある。 - 独自仕様としてオブジェクトを一階層挟み込むやり方も、よく見かける。
例:{"class_name":{"member_name":"value"}}
- しかし、使わないに越したことは無い。そもそもコンテキストによって明らかなことが多い。本当に型が必要であれば、XML を使った方がよい。
- JSON-LD には同種の仕組みとして
@context
を置く仕様がある。
Property path
jq
などのツールを使うとプロパティへのアクセス経路を文字列で書くことがある。
例えば次のようなオブジェクトの場合、.hello.world
と辿ることで 1
という値にたどり着く。この .hello.world
のような表記を Property path と呼んだりする。access chaining と呼んでも通じるだろう。JsonPath という仕様 に落としこんでいる人もいる。
{
"hello": {
"world": 1
}
}
JSON は汎用コンテナなので、先ほどの .hello.world
に文字列を格納することもできる。
{
"hello": {
"world": "say hello"
}
}
このようにJSONの型を利用するのは一見便利だけれど、問題を起こしやすいことに注意。
具体的を挙げてみる。例えば Elasticsearch 検索エンジンでは型を使って検索インデックスを構築する。ここで同じ Property path に異なる型が登録されるとエラーを起こす。
こんな形式のデータは問題を起こしやすい。
{
"values": [
{ "type":"int", "value": 1 },
{ "type":"string", "value": "hello" }
]
}
これは例えばこうした方がいい。property path に対しては型を安定させた方がいい。
{
"values": [
{ "int_value": 1 },
{ "string_value": "hello" }
]
}
json pointer
json schema でも使われている仕様で、URI の fragmentation と property path を合体させたような仕様。RFC 6901 になっている。
ドキュメント本体は #
で指し示して、property path を /
区切りで連結させる。#hello/world
のように指定できるほか、json document 自体の URL があった場合に http://example.org/jsonfile#hello/world
と絶対 URL 形式で、オブジェクトの中身を指定できる。
json schema
もう XML 使えばいいんじゃないかと思う一方で、絶妙な中途半端さ加減で json schema という仕様があり、json の型検査ができる。わりと使える。
仕様の draft の版に注意すること。
swagger/OpenAPI で、より詳細な型情報を定義したいときなど、強化目的で使ったりする。全体を json schema で取り組もうとすると、ボリュームが大きく感じるだろう。Avro の schema から json schema への変換といったこともできるので、中間表現としてとらえるのがよいだろう。
API仕様
API で使うという場合によく出てくるのは、次の二つだろう。
- REST API (HTTP)
- json-rpc
おおむね REST API のほうが使い勝手は良い。json-rpc over tcp の notification が特徴的。逆向きの通知ができるメリットがある一方で、冗長化が難しくなったりするデメリットもある。NAT などのネットワーク的な条件で json-rpc が適しているときがある。
REST API は api blueprint や swagger 等の記述方法を使うと定義が安定しやすい(メンバー間での認識が一致しやすい)ようだ。swagger は OpenAPI という名前で参照されたりもする。json-schema の拡張仕様である json-schema-hyperschema も良い。
REST API での HTTP リソース表現形式に悩むような場合は HALなどの追加仕様 を導入する人もいるようだ。Downside も大きいので、個人的には採用したことは無いけれど…。
JSON Stream
JSON 行ベースでひたすら書き出すということも、よくよく起こることで、これを再整形して処理したいケースもよく起こる。
RFC7464 になっている application/json-seq
という仕様では、ASCII コード RS (\x1E
) で始まり、改行 LF で終わるユニットで数える。jq
コマンドの --seq
オプションはこれに対応したもの。
例えば journalctl と組み合わせると、こんな使い方ができる。
journalctl -u some_app -o cat | sed 's/^/\x1e/' | jq --seq .
Server-Sent Events という仕様もある。こちらはストリームの区切りをより強くハンドリングできるようになっている。ちなみに journalctl でもこの出力形式がサポートされた。
JSON patch
行指向なテキストに diff
が便利なように、json にも diff
があると便利。RFC 6902とサイトがある。
REST API で PATCH method で json representation の更新を行うように実装したい、というケースもよくある。この JSON Patch 以外にも JSON Merge Patch という表現方法もある。
署名
OpenPGP に sjson
が記載されている。simplestream で使われていたりする。
Unix console
CLI も試行錯誤されていろいろある
- jq : 標準出力からの整形に便利
-
python -m json.tool
:jq
を入れるほどでもなく整形したいときに便利 - jo : json を組み立てる際に使える
- httpie : REST API / json で便利に使える。python venv に pip install でも便利。
親戚
可読性を犠牲にしてコンパクトにしたい場合は msgpack を使うとよい。UDP パケットに入れたいときとか。
雑学
JSON ライセンスという evil なライセンスがある…