JerseyでJSONを扱うときは注意

JAX-RSは素晴らしい仕様です。

しかし、その参照実装であるJerseyでJSONを扱うときには注意が必要です。
けっこうイヤな動きをします。

Listオブジェクトをエンコードしてくれない

トップレベルのオブジェクトがListの場合、JSONにしてくれません。
他のオブジェクトのプロパティとしてListを持たせる分にはOKです。

Listの要素数JSONでの型が変わる

  • 素数が0のとき⇒null
  • 素数が1のとき⇒要素そのもの
  • 素数が2以上のとき⇒配列

となります。

・・・この仕様が嬉しい人いたら教えて下さい。

サンプル

文字列の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でした。