JSON-Bの環境設定と簡単な実装例
はじめに
前回、Java EE 8が出たので、ちなんだ機能を使用してみようかな?というところで終えてました。
ちなみにJava EE 8で新設された機能は次の2つです。
できればSecurity APIの使い勝手をレビューしたかったのですが、シンプルと謳う割には面倒で
(正確には、基本は簡単。応用になるといきなりわからなくなる感じ)
1か月たってもまとめられなかったので、JSON-Bのほうから見てみようかと思います。
環境設定
依存関係のビルド設定
公式サイトを参考に設定します。公式のほうはMaven形式の記載ですが、ここではGradle形式で記載します。
repositories {
maven {
url "https://maven.java.net/content/groups/public/"
}
mavenCentral()
mavenLocal()
}
dependencies {
// for json-b
compile group: 'javax.json.bind', name: 'javax.json.bind-api', version: '1.+'
compile group: 'org.eclipse', name: 'yasson', version: '1.+'
compile group: 'org.glassfish', name: 'javax.json', version: '1.+'
}
依存関係を更新すると、次がDLされます。
Download https://repo1.maven.org/maven2/org/eclipse/yasson/1.0.0-RC2/yasson-1.0.0-RC2.jar
Download https://maven.java.net/content/groups/public/javax/json/bind/javax.json.bind-api/1.0.0-RC2/javax.json.bind-api-1.0.0-RC2.jar
Download https://maven.java.net/content/groups/public/org/glassfish/javax.json/1.1.0-SNAPSHOT/javax.json-1.1.0-20161201.183011-5.jar
Download https://maven.java.net/content/groups/public/javax/json/javax.json-api/1.1/javax.json-api-1.1.jar
Download https://repo1.maven.org/maven2/javax/enterprise/cdi-api/2.0/cdi-api-2.0.jar
Download https://maven.java.net/content/groups/public/javax/el/javax.el-api/3.0.0/javax.el-api-3.0.0.jar
Download https://maven.java.net/content/groups/public/javax/interceptor/javax.interceptor-api/1.2/javax.interceptor-api-1.2.jar
Download https://repo1.maven.org/maven2/javax/inject/javax.inject/1/javax.inject-1.jar
実装サンプル
公式サイトでは、ジェネリクス付きコレクションのサポートのサンプルが記載されていますが
よく使いそうなMapのサポートがあることの記載がないので、以下にJSON文字列からMapへのパースの例を記載しておきます。
// Create Jsonb and serialize
Jsonb jsonb = JsonbBuilder.create();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
InputStream is = classLoader.getResourceAsStream("test.json");
Type hashMapType = new HashMap<String, Object>() {}.getClass().getGenericSuperclass();
try {
Map<String, Object> jsonMap = jsonb.fromJson(is, hashMapType);
System.out.println("Map-->:" + jsonMap);
} catch (JsonbException e) {
e.printStackTrace();
}
{
"title" : "Map parse Test",
"array": [null, 12],
"object": {
"id": false
}
}
実行結果
Map-->:{array=[null, 12], title=Map parse Test, object={id=false}}
ちなみに、おおもとのJsonbを取り出すJsonbBuilder.create()はインタフェースのデフォルトメソッド機能を使っているので、JDK8以上でないと機能しないです。ご注意を。
今まで使用していたその他のパーサとの比較をしてみる
4つのライブラリで比較する
さて、入門編はこの程度にして、ここからは今後のシステム開発で使えそうな内容を残しておきます。
JSON-Bとして標準API化されましたが、
いったいどこまでJSON仕様用に則ってパースされるのか
その他のライブラリとの違いは?
などを比較しながら確認してみました。
対象としたライブラリは、Jackson、JSONIC、Gsonの3ライブラリと*JSON-B*で、同じファイルをパースした場合の挙動を比較しています。
自身のプロジェクトでも使えるように、システムを作成するうえで使用するライブラリを検討する資料(別紙扱い)として、そのまま方式設計に記載できたら良いなと思って書いてます。
比較元となる仕様(ECMA-404/RFC 7159)
前提として、次の2種類の仕様をもとに、パース結果を比較します。
比較した環境
それぞれのライブラリを取得し
JSONファイルからMap<String,Object>へパースするメソッドとその逆を用意し
実行結果を比較しています。
- Jacksonの実装
static final ObjectMapper MAPPER = new ObjectMapper();
@Override
public Map<String, Object> fromJsonMap(InputStream is) {
TypeReference<Map<String, Object>> ref = new TypeReference<Map<String, Object>>() {};
Map<String, Object> res = new HashMap<>();
try {
res = MAPPER.readValue(is, ref);
} catch (IOException e) {
e.printStackTrace();
}
return res;
}
@Override
public String toJsonMap(Map<String, Object> map) {
String res = "";
try {
res = MAPPER.writeValueAsString(map);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return res;
}
- JSONICの実装
static final Type HASH_MAP_TYPE = new HashMap<String, Object>() {}.getClass().getGenericSuperclass();
@Override
public Map<String, Object> fromJsonMap(InputStream is) {
Map<String, Object> res = new HashMap<>();
try {
res = net.arnx.jsonic.JSON.decode(is, HASH_MAP_TYPE);
} catch (JSONException | IOException e) {
e.printStackTrace();
}
return res;
}
@Override
public String toJsonMap(Map<String, Object> map) {
String res = "";
res = net.arnx.jsonic.JSON.encode(map);
return res;
}
- Gsonの実装
static final Gson GSON = new Gson();
static final Type MAP_TYPE = new TypeToken<Map<String, Object>>() {}.getType();
@Override
public Map<String, Object> fromJsonMap(InputStream is) {
InputStreamReader isr = new InputStreamReader(is, Charset.forName("UTF-8"));
return GSON.fromJson(isr, MAP_TYPE);
}
@Override
public String toJsonMap(Map<String, Object> map) {
return GSON.toJson(map, MAP_TYPE);
}
- JSON-Bの実装
static final Type HASH_MAP_TYPE = new HashMap<String, Object>() ヴぇ{}.getClass().getGenericSuperclass();
static final javax.json.bind.Jsonb JSONB = JsonbBuilder.create();
@Override
public Map<String, Object> fromJsonMap(InputStream is) {
return JSONB.fromJson(is, HASH_MAP_TYPE);
}
@Override
public String toJsonMap(Map<String, Object> map) {
return JSONB.toJson(map, HASH_MAP_TYPE);
}
[比較1] パース後の型
まず、確実にパースできるところから(objectのパースから)各値のパース結果のJavaの型を比較しました。
ファイル内の値 | JSON-B | Jackson | JSONIC | GSON |
---|---|---|---|---|
”....” | java.lang.String | java.lang.String | java.lang.String | java.lang.String |
-9 | java.math.BigDecimal | java.lang.Integer | java.math.BigDecimal | java.lang.Double |
-9.0 | java.math.BigDecimal | java.lang.Double | java.math.BigDecimal | java.lang.Double |
true | java.lang.Boolean | java.lang.Boolean | java.lang.Boolean | java.lang.Boolean |
fale | java.lang.Boolean | java.lang.Boolean | java.lang.Boolean | java.lang.Boolean |
null | null | null | null | null |
[.., ..] | java.util.ArrayList | java.util.ArrayList | java.util.ArrayList | java.util.ArrayList |
{.., ..} | java.util.HashMap | java.util.LinkedHashMap | java.util.LinkedHashMap | com.google.gson.internal.LinkedTreeMap |
個人評価 | △ | - | 〇 | 〇 |
数値は、JSON-BがBigDecimalなのに対し、JSONIC以外はDoubleやIntegerをつかっています。
オブジェクトはJSON-B以外はLinkedHashMap, LinkedTreeMapを使用していました。
個人的には、数値はBigDeimal, パースした結果は検索することのほうが多いと思いますのでTreeMapがよいかなぁと感じています。
[比較2] 仕様に則ったシーケンスのパース比較(最小構成の評価)
前提としている仕様を読んでみると、最小単位が値であって、{}や[]で囲まなくともパースできるようなことが書かれています。
- ECMA-404の仕様
A JSON text is a sequence of tokens formed from Unicode code points that conforms to the JSON value grammar.
(JSONテキストは、JSON値の文法に準拠したUnicodeコードポイントから形成されたトークンのシーケンスです。)
* トークン:ソースコードの単位要素ソースコードを構成する最小単位の要素のこと。変数名や予約語、演算子、定数などがこれに相当する。
* シーケンス:処理の順番の並びやデータの順番の並び
- RFC 7159の仕様
A JSON text is a sequence of tokens. The set of tokens includes six structural characters, strings, numbers, and three literal names.
(JSONテキストは一連のトークンです。トークンのセットには、6つの構造文字、文字列、数字、および3つのリテラル名が含まれます。)
実践ではあまり使われないかもしれませんが、最小構成の評価としてパース結果を見てみます。
ファイル内は~のみの構成 | JSON-B | Jackson | JSONIC | GSON |
---|---|---|---|---|
”....” | X | X | X | X |
-9 | X | X | X | X |
-9.0 | X | X | X | X |
true | X | X | X | X |
fale | X | X | X | X |
null | X | null | null | null |
[.., ..] | X | X | OK | X |
{.., ..} | OK | OK | OK | OK |
0バイトファイル | X | X | 無 | null |
個人評価 | - | - | 〇 | △ |
Xはそれぞれパース時に例外などが発生し失敗したことを示しています。
JSON-Bは「Error deserialize JSON value into type」となり、
Jacksonは「MismatchedInputException」例外
JSONICは例外内容が「null」とだけ表示されます。
GSONは「java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING」と評価。開始の文字列として不適当と判断されているようです。
[]から始まるファイルは、JSONICだけがパースできました。Mapに入れようとしていたこともあり、キー:0で値が記述されている値で登録されました。
あれ、これだけ見ると、ListやStringなど、それぞれにおいて適切なクラスでパースすれば他のライブラリもパースできるかもしれませんね。。。。今度やってみましょう。
nullという文字はJSON-B以外ではnullとして登録されます。
0バイトファイルはJSONICやGSONではきちんと評価されました。(JSONICはsize 0のMapオブジェクトが作成されたので無)
[比較3] 文字のパース比較
ここからは、それぞれの要素内での比較をしていきます。
文字列は、次のような仕様になってます。
エンコードはUTF-8が基本。エスケープしてほしいものは\つけて表現してほしいみたいです。
改行コードや、タブなどはかなりざっくりとみて、4桁UNICODE文字とかはちょっと頑張って確認しました。
(基本ダブルウォートで囲むんですね。。。)
文字列の種類 | JSON-B | Jackson | JSONIC | GSON |
---|---|---|---|---|
" | OK | OK | OK | OK |
\|OK | OK | OK | OK | |
/ | OK | OK | OK | OK |
\b | OK | OK | OK | OK |
\f | OK | OK | OK | OK |
\n | OK | OK | OK | OK |
\r | OK | OK | OK | OK |
\t | OK | OK | OK | OK |
\uFFFF | OK | OK | OK | OK |
\uFFFF\uFFFF(サロゲート文字) | OK | OK | OK | OK |
ダブルクォートで囲まずに\uでnull文字を表示 | X | X | OK | X |
{を\u文字で表示 | X | X | X | X |
タブ文字や改行文字をエスケープしないで表示 | X | X | OK | OK |
★オブジェクトのキー側をダブルクォートで囲まない | X | X | OK | OK |
値をシングルクォートで囲む | X | X | OK | OK |
キーをシングルクォートで囲む | X | X | OK | OK |
個人評価 | - | - | 〇 | 〇 |
結果、JSON-Bは、仕様に則っているのですが、できる範囲が多いほうを評価してしまいます。
JSONICは\uで書いたnull文字(\u006e\u0075\u006c\u006c)ですら、nullで表示されました。(すごい)
オブジェクトのキー側はいちいちダブルクォートで囲みたくはないので、たとえ仕様でも、囲まないでもきちんとパースできるほうが嬉しいですねぇ。
同様にシングルクォートで囲んでもJSONIC, GSONにおいては問題なくパースしてくれました。
[比較4] 数値のパース比較
数値は、複雑そうなレールダイアグラムの割には、ほとんど差がなく、どのライブラリも対応していました。
数値文字の種類 | JSON-B | Jackson | JSONIC | GSON |
---|---|---|---|---|
1 | OK | OK | OK | OK |
1.8 | OK | OK | OK | OK |
-8 | OK | OK | OK | OK |
-0.1 | OK | OK | OK | OK |
1e1 | OK | OK | OK | OK |
1E+0 | OK | OK | OK | OK |
-8e-2 | OK | OK | OK | OK |
0.0 | OK | OK | OK | OK |
-10.005E+12 | OK | OK | OK | OK |
-0 | OK | OK | OK | OK |
.1 | X | X | X | String |
.8E6 | X | X | X | String |
個人評価 | - | - | - | 〇 |
GSONだけ.からの数値に対応している!と思いきや文字列になってました('ω')
[比較5] リテラルのパース比較
リテラルは、3種類「true」「false」「null」のことで、小文字であることが仕様としては条件ですが
条件外の値もどれだけパースできるかを確認してみました。
リテラル文字の種類 | JSON-B | Jackson | JSONIC | GSON |
---|---|---|---|---|
true | OK | OK | OK | OK |
false | OK | OK | OK | OK |
null | OK | OK | OK | OK |
True | X | X | OK(String) | OK(Boolean) |
False | X | X | OK(String) | OK(Boolean) |
Null | X | X | OK(String) | OK(null) |
"true" | OK(String) | OK(String) | OK(String) | OK(String) |
nil | X | X | OK(String) | OK(String) |
nul | X | X | OK(String) | OK(String) |
\0 | OK | OK | OK | OK |
個人評価 | - | - | - | 〇 |
今度はGSONだけ、大文字から始まるTrue/False/NULLをBoolean/nullと変換されました。('ω')?
いい意味で柔軟?
[比較6] 配列、オブジェクトのパース比較
同様に配列、オブジェクトのパース結果は以下の通りとなります。
配列/オブジェクトの種類 | JSON-B | Jackson | JSONIC | GSON |
---|---|---|---|---|
型が混在しそうな配列 | OK | OK | OK | OK |
多段配列 | OK | OK | OK | OK |
最後が空要素(,終わり)の配列 | OK | OK | OK(null) | OK(null) |
最初も空要素(,始まり)の配列 | X | X | OK(null) | OK(null) |
中身なしオブジェクト、および多段オブジェクト | X | X | OK(String) | OK(Boolean) |
値が空のキーが存在するオブジェクト | X | X | OK(null) | X |
カンマ(,)始まりのオブジェクト | X | X | OK(null) | X |
個人評価 | - | - | 〇 | 〇 |
前、後ろにカンマがあった場合でもJSONICとGSONは変換してあげています。
[比較おまけ] 重複登録ができるのか
最後、おまけに重複キーがある場合のオブジェクトの変換、カンマ終わりのオブジェクト
それぞれのパース結果がどうなるのか、、、
オブジェクト | JSON-B | Jackson | JSONIC | GSON |
---|---|---|---|---|
重複なキーが存在するオブジェクト | OK | OK | OK | X |
★カンマ(,)終わりオブジェクト | X | X | X | X |
はい。今までとは反対にGSONだけ重複キーがあると怒られました。これはこれで使えそう
オブジェクトのカンマ終わりは全部のライブラリでやっぱりダメ見たいです
参考資料
今まで比較した資料はgithubにあげています。
比較はJUnitを使いました。(assert機能は使ってないけれど)
クラス | 内容 |
---|---|
org.JsonBaseValue | パース後の型比較、重複チェック |
org.JsonGrammerCompString | 文字列比較 |
org.JsonGrammerCompNumber | 数値の比較 |
org.JsonGrammerCompLiteral | リテラルの比較 |
org.JsonGrammerCompArrayObj | 配列とオブジェクトの比較 |
総評
JSON-Bはかなり仕様に忠実にパースされているため、厳密にJSONを検証したり、正確なJSONを返却する用途に向いていそう。
一方で、ユーザが自由に値を入れ込む設定ファイルのような使い方をする場合は、ある程度緩く作られているJSONIC、GSONが向いてそう。
(個人的には特に★オブジェクトのキー側をダブルクォートで囲まないが実現していてほしい。キーのほうは、ブランク入れない限りはすべてクォートで囲めなんてしんど過ぎ)
JSONICとGSONどちらを使用するのかは、どちらも良い悪いがあったので、個人の判断に任せます。
syanyが提供しているライブラリ(uranoplums)はGSONを選んでましたが、JSONICもいいなぁと今回ので、考えさせられました。
すべてのライブラリで直してほしいのは★カンマ(,)終わりオブジェクトが認められていないというところ。。。もうそろそろそこは緩くしないか?そんなに言うのならyamlを使え?もっともだね。。。
かなり長くなってしまったので、この辺で、、次回ってあるかな、、、