JerseyでJSONを扱うときは注意
JAX-RSは素晴らしい仕様です。
しかし、その参照実装であるJerseyでJSONを扱うときには注意が必要です。
けっこうイヤな動きをします。
サンプル
文字列のListをJSONで返すコードを書いてみます。
直感的な動作は
return Arrays.<String>asList();
のときは
[ ]
return Arrays.<String>asList("a");
のときは
["a"]
return Arrays.<String>asList("a", "b");
のときは
["a", "b"]
となることでしょう。
実際にやってみます。
まず、JerseyはListを扱えないのでラップするクラスを作ります。この時点で信じられないめんどくささです。
@XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) static class StringList { @XmlElement List<String> list; StringList() { } StringList(final List<String> pList) { this.list = pList; } }
内部クラスとして作ったのでクラス宣言にstaticが付いています。
あと、引数なしのコンストラクタはプログラムからは使わないけど必要です。
さてStringのListを返して、JSONがどうなるかを見てみます。
return new StringList(Arrays.<String>asList());
のときは
null
・・・は?null?トップレベルのオブジェクトがなくなったぞ?
return new StringList(Arrays.<String>asList("a"));
のときは
{"list":"a"}
ここ要注意!listプロパティの値が単なる文字列になってます!Listはどこに消えたんだ?!
return new StringList(Arrays.<String>asList("a", "b"));
のときは
{"list":["a","b"]}
ようやく期待する形(に近い形)でJSONが得られました。
対策・JsonMessageBodyWriterを作る
要素数でJSON側の型が変わるのは、JavaScriptで処理するときのことを考えるとかなり使いにくいです。
そこでJSONICを使って自前でMessageBodyWriterを作り、JSON側では常に配列となるようにします。
import java.io.IOException; import java.io.OutputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.ext.MessageBodyWriter; import javax.ws.rs.ext.Provider; import net.arnx.jsonic.JSON; /** * @author jabaraster */ @Provider public class JsonMessageBodyWriter implements MessageBodyWriter<Object> { @SuppressWarnings({ "unused", "javadoc" }) @Override public long getSize(final Object pT, final Class<?> pType, final Type pGenericType, final Annotation[] pAnnotations, final MediaType pMediaType) { return -1; } @SuppressWarnings({ "unused", "javadoc" }) @Override public boolean isWriteable(final Class<?> pType, final Type pGenericType, final Annotation[] pAnnotations, final MediaType pMediaType) { return MediaType.APPLICATION_JSON_TYPE.isCompatible(pMediaType); } @SuppressWarnings({ "unused", "javadoc" }) @Override public void writeTo( // final Object pT // , final Class<?> pType // , final Type pGenericType // , final Annotation[] pAnnotations // , final MediaType pMediaType // , final MultivaluedMap<String, Object> pHttpHeaders // , final OutputStream pEntityStream // ) throws IOException, WebApplicationException { JSON.encode(pT, pEntityStream, true); } }
超シンプル。なんと3つのメソッド全てが、たった1行の実装です。
だけど期待通りの結果が得られます。
その上読みやすい形でJSONが得られます(通信量増えるけど)。
Jerseyのバージョン
jersey-server及びjersey-jsonのバージョンは、共に1.8でした。