jackson-jqを使ってJavaでjqクエリを処理する

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

よく訓練されたアップル信者、都元です。APIのデータ入出力フォーマットとして、JSONはすっかり市民権を得た感がありますね。私、XMLも意外と好(もごもご

Jacksonで目的の値を取り出す

さて、Javaの世界において、よく使われているJSON処理ライブラリとしてJacksonというものがあります。

ここでは、OpenWeatherMapの出力を使って説明していきます。(ただし、以下を実際に試してみるためには、OpenWeatherMapに対して無料登録をおこなって、APIキーを手に入れる必要があります。)

http://api.openweathermap.org/data/2.5/weather?APPID=XXXXAPIKEYXXXX&q=Tokyo,jp

上記URLにアクセスすることによって、下記のような情報が得られます。

{
  "coord": {
    "lon": 139.69,
    "lat": 35.69
  },
  "weather": [
    {
      "id": 801,
      "main": "Clouds",
      "description": "few clouds",
      "icon": "02d"
    }
  ],
  "base": "stations",
  "main": {
    "temp": 306.47,
    "pressure": 1011,
    "humidity": 62,
    "temp_min": 301.15,
    "temp_max": 312.59
  },
  "visibility": 10000,
  "wind": {
    "speed": 3.1,
    "deg": 140
  },
  "clouds": {
    "all": 20
  },
  "dt": 1467856812,
  "sys": {
    "type": 1,
    "id": 7619,
    "message": 0.0223,
    "country": "JP",
    "sunrise": 1467833520,
    "sunset": 1467885609
  },
  "id": 1850147,
  "name": "Tokyo",
  "cod": 200
}

このJSONは、Jacksonで以下のように処理できます。

ObjectMapper om = new ObjectMapper();
URL url = new URL("http://api.openweathermap.org/data/2.5/weather?APPID=" + API_KEY + "&q=Tokyo,jp");
JsonNode weatherDocument = om.readTree(url);

ObjectMapper というのが、Jacksonの本体という認識で構いません。readTree メソッドにURLを渡すことによって、そのコンテンツを読み込みます。ちなみに、ここはStringでやInputStream等、一般的な入力には一通り対応しています。

log.info("weatherDocument = {}", weatherDocument);
Number temp = weatherDocument.get("main").get("temp").numberValue();
log.info("temp = {}", temp);
for(JsonNode weather : weatherDocument.get("weather")) {
    log.info("weather = {}", weather.get("main").textValue());
}

上記の出力例はこんなかんじですね。

weatherDocument = {"coord":{"lon":139.69,"lat":35.69}...(略)
temp = 306.47
weather = Clouds

得られた weatherDocument をそのまま文字列として出力することも可能です。そしてそのドキュメントの中から特定の値を取り出すには、こんな操作を行います。まぁ、XMLにおけるDOMのような要領で、なんとなく理解できると思います。

Javaでjqクエリ言語を使う

さて、jq使ってますか? curlコマンド等得たHTTPのJSONレスポンスをパイプで受け取って、整形したり欲しいものだけを取り出したりする コマンドラインツール (CLI) です。私は便利に使わせてもらっています。jqのクエリ言語は、個人的に好きで勉強した結果、XMLに対するXPathのような感覚でかなり自由に使いこなせるようになりました。

上記で紹介している通り、あくまでもjqが活躍する場は、パイプ等を駆使したシェルやシェルスクリプトの世界です。

なのですが、例えばサーバーサイドでJSONをゴニョゴニョしなければならない時に「さっとjqクエリ言語を書いて処理を定義できたらなー」と、ずっと思っていました。要するに、jqクエリ言語の処理系をJavaに移植するって話です。そのためには、javacc等を使ってクエリ言語のコンパイラを書かなきゃいけないわけです。正直しんどい。

と思っていたら、どうやら作ってくれちゃった人がいました。…しかもどうやら日本のお方じゃないですか。本当にどうもありがとうございます。もし何かありましたら、PRでコントリビュートさせていただこうと思います。(私信)

eiiches/jackson-jq: jq for Jackson Java JSON Processor

さて、このjackson-jqを使えば、Jacksonで読み込んだ JsonNode に対して JsonQuery を適用して List<JsonNode> を得る、ということができます。具体的にはこんな感じ。

JsonQuery q = JsonQuery.compile("{tenkis:[.weather[].main], ondo:.main.temp}");
List<JsonNode> results = q.apply(weatherDocument);
JsonNode result = results.get(0);

result にはこんな感じのJSONが入っています。

{"tenkis":["Clouds"],"ondo":306.53}

余談

jqは「JSONからJSONを作る」という説明が多いと思います。が、正確に言うと、JSON列(複数のJSONが連なったもの)からJSON列を作る仕組みです。下記の例で、jqへの入出力は、共に「JSON」ではありません。「JSON列」です。

$ echo '{"foo":{"bar":1}}{"foo":{"bar":2}}' | jq -c .foo
{"bar":1}
{"bar":2}

jackson-jq において apply の出力が List<JsonNode> となっているのはこのような事情があるからなんですね。

さらに余談ですが「JSON列」の特殊なものに「JSON Lines」という仕様もあります。上記の例で、入力は(残念ながら)JSON Linesではありませんが、出力はJSON Linesになっていると思います。

色々仕様を漁っていると面白いものがいっぱいあるもんですねー。