倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

Gson で Scala のコレクションを使う

前回、Google が開発している Java 用の JSON ライブラリを Scala で使う方法を見ました。 ただし、Scala のコレクションを Gson で使えるようにする仕方がよく分からなかったので、結局配列や Java のコレクションを使っていました。 あの後、あれこれ試してたら Scala のコレクションを使う方法が分かったので、この記事ではそれを見ていきます。 ただし、ちょっと設定が必要なので、よほど Scala のコレクションが必要な場合で無い限りこの方法を使うことはあまりないかと思います。

この記事の目次

参考


カスタマイズした Gson オブジェクトを使う

前回見たように、Java オブジェクトと JSON オブジェクトを互いに変換するためには Gson オブジェクトを使います。 ただし、変換に関する設定はこのオブジェクト自体にはできず、GsonBuilder を介して行います。 手順としては

  1. GsonBuilder オブジェクトを生成する
  2. GsonBulder オブジェクトに設定を施す
  3. GsonBuilder#create() メソッドによって Gson オブジェクトを生成する

という風にします。 生成された Gson オブジェクトの使い方は前回と同じで、fromJson(), toJson() メソッドを使って変換を行います。

GsonBuilder に施せる設定にはいろいろあるようですが、Scala のコレクションを使えるようにするための設定は

  1. GsonBuilder#registerTypeAdapter(Class, AnyRef)
  2. GsonBuilder#registerTypeHierarchyAdapter(Class, AnyRef)

で行います。 第2引数の AnyRef には Java オブジェクト <=> JSON オブジェクトの変換を行う処理を実装したクラスのオブジェクトを渡します。 型は AnyRef ですが、次のインターフェースのいずれか(もしくは複数)を実装したクラスを渡す必要があります:

  • JsonSerializer<E> ・・・ E 型の Java オブジェクト => JSON オブジェクトの変換
  • JsonDeserializer<E> ・・・ JSON オブジェクト => E 型の Java オブジェクトの変換
  • InstanceCreator<E> ・・・ E 型の Java オブジェクトの生成

InstanceCreator は引数無しのコンストラクタから空のコレクションを生成するのに使うようですが、いまいち動かなかったのでこの記事では省略。 GsonBuilder#registerXxxx() メソッドの第1引数の Class は、これらの変換を適用するオブジェクトの型です。 この記事では scala.collection.immutable.List などです。

registerTypeAdapter() メソッドと registerTypeHierarchyAdapter() メソッドの違いは、registerTypeAdapter() メソッドで登録した変換は第1引数のクラスと完全に等しいクラスを持つオブジェクトに対してのみ有効なのに対して、registerTypeHierarchyAdapter() メソッドで登録した変換は第1引数のクラスを実装するオブジェクト全てに対して有効であることです。 前回うまく行かなかったのはこれのせいでした。

では具体的に設定の方法と使い方を見ていきましょう。

Scala コレクションを JSON に変換する

まずは Scala のコレクションを JSON に変換できるようにしてみましょう。 必要なのは JsonSerializer インターフェースの実装クラスです。 Scala のコレクションは全て GenTraversableOnce トレイトを拡張(ミックスイン?)しているので、JsonSerializer の型パラメータは GenTraversableOnce[_] にしておきましょう。

import com.google.gson._
import scala.collection.GenTraversableOnce
import java.lang.reflect.Type

/** Scala コレクションを JSON に変換する */
class ScalaCollectionSerializer extends JsonSerializer[GenTraversableOnce[_]]{

  override def serialize(src: GenTraversableOnce[_],
                         typeOfSrc: Type,
                         context: JsonSerializationContext): JsonElement =
    // Scala コレクションを Java の配列にしてから JSON に変換
    context.serialize(src.toArray, classOf[Array[_]])
}

object Main extends App{
  // 準備
  val gb = new GsonBuilder
  // registerTypeHierarchyAdapter() メソッドで登録することによって
  // GenTraversableOnce のサブクラス全てについて(よって Scala コレクション全てについて)
  // 変換が行われる
  gb.registerTypeHierarchyAdapter(classOf[GenTraversableOnce[_]], new ScalaCollectionSerializer)
  val gson = gb.create()

  // List => JSON
  val intList = List(1, 2, 3, 4)
  val json1 = gson.toJson(intList)
  println(json1)  // 「[1,2,3,4]」と表示

  // Set => JSON
  val strSet = Set("abc", "def")
  val json2 = gson.toJson(strSet)
  println(json2)  // 「["abc","def"]」と表示
}
  • 実際に行っている変換は、Scala コレクションを Java の配列に変換して、その配列をデフォルトの変換によって JSON へと変換するというものです。
  • JsonSerializationContext#serialize() メソッドによって Gson#toJson () メソッドの変換が行えます。 登録した変換も有効になっているので、要素に Scala コレクションが出てきても OK!
  • JsonSerializer オブジェクトを registerTypeHierarchyAdapter() メソッドで登録することによって、GenTraversableOnce トレイトの全てのサブクラスに対して変換が行われます。 Scala のコレクションは全てこのトレイトをミックスインされているので、結局、全ての Scala コレクションに対してこの変換が行われます。
  • Gson オブジェクト自体の使い方は前回と同じなので説明略

Gson オブジェクトを使って変換を行っている部分のコードはまさに望んでいたものそのもの!

さて、ついでにプロパティとして Scala のコレクションを持つオブジェクトがキチンと JSON に変換されるかも試しておきましょう。 前回使った Person クラスみたいなやつね。 まぁ、下に定義書きますが。

/**  alias プロパティが List 型 */
case class Person(name: String, age: Int, alias: List[String])

object Main extends App{
  // 準備
  val gb = new GsonBuilder
  gb.registerTypeHierarchyAdapter(classOf[GenTraversableOnce[_]], new ScalaCollectionSerializer)
  val gson = gb.create()

  // Person => JSON
  val waman = Person("waman", 100, List("god", "devil"))
  val json3 = gson.toJson(waman)
  println(json3)  // 「{"name":"waman","age":100,"alias":["god","devil"]}」と表示
}

とまぁ、問題なく動きます。

JSON を Scala コレクションに変換する

次は JSON を Scala コレクションに変換できるようにしてみましょう。 実装が必要なのは JsonDeserializer インターフェース。 Gson#fromJson() メソッドによる変換では List や Set がほしいので、JsonDeserializer の型パラメータに GenTraversableOnce を指定するのはマズいですね。 ここでは List と Set に対する JsonDeserializer を2つ書くことにします*1

import com.google.gson._
import java.util
import java.lang.reflect.Type
import scala.language.implicitConversions
import scala.collection.JavaConversions._

/** JSON を List に変換する */
class ScalaListDeserializer extends JsonDeserializer[List[_]]{

  override def deserialize(json: JsonElement,
                           typeOfT: Type,
                           context: JsonDeserializationContext): List[_] =
    // Java の List として構築してから Scala のコレクションに変換
    context.deserialize[util.List[_]](json, classOf[util.List[_]]).toList
}

/** JSON を Set に変換する */
class ScalaSetDeserializer extends JsonDeserializer[Set[_]]{

  override def deserialize(json: JsonElement,
                           typeOfT: Type,
                           context: JsonDeserializationContext): Set[_] =
    context.deserialize[util.List[_]](json, classOf[util.List[_]]).toSet
}

object Main extends App{
  // 準備
  val gb = new GsonBuilder
  // ScalaListDeserializer で List オブジェクトだけでなく、
  // Seq オブジェクトなども取得できるようにする
  gb.registerTypeHierarchyAdapter(classOf[GenTraversableOnce[_]], new ScalaListDeserializer)
  // ScalaSetDeserializer は Set オブジェクトだけ
  gb.registerTypeAdapter(classOf[Set[_]], new ScalaSetDeserializer)
  val gson = gb.create()

  // JSON => List
  val result1 = gson.fromJson("[1, 2, 3, 4, 5]", classOf[List[Int]])
  println(result1)  // 「List(1.0, 2.0, 3.0, 4.0, 5.0)」と表示

  // JSON => Set
  val result2 = gson.fromJson("""["abc", "def"]""", classOf[Set[String]])
  println(result2)  // 「Set(abc, def)」と表示

  // JSON => Seq
  val result3 = gson.fromJson("[1, 2, 3]", classOf[Seq[Int]])
  println(result3)  // 「List(1.0, 2.0, 3.0)」と表示
}
  • ScalaListDeserializer を GenTraversableOnce クラスとともに registerHierarchyAdapter() メソッドで登録することで、Gson#fromJson() メソッドを使う際に List オブジェクトだけではなく GenTraversableOnce オブジェクトや Seq オブジェクトも取得できるようになります。 まぁ、単に List オブジェクトが返されるのでキャストできるってだけですけど・・・
  • JsonDeserializer では JsonSerializer の時とは違って、1度 Java の List に変換してから Scala のコレクションに変換しています。 配列に変換しようとすると例外が投げられます。

JSON から Scala コレクションへの変換コードも期待したように書けますね。

Scala コレクションをプロパティに持つ場合もやっておきましょう。

case class Person(name: String, age: Int, alias: List[String])

object Main extends App{
  // 準備
  val gb = new GsonBuilder
  gb.registerTypeHierarchyAdapter(classOf[GenTraversableOnce[_]], new ScalaListDeserializer)
  gb.registerTypeAdapter(classOf[Set[_]], new ScalaSetDeserializer)
  val gson = gb.create()

  // JSON => Person
  val me = gson.fromJson(json3, classOf[Person])
  println(me)  // 「Person(waman,100,List(god, devil))」と表示
}

とまぁ、問題なく変換できます。

さて、Gson で Scala のコレクションを使う方法を見てきましたが、GsonBuilder の設定さえやっておけば実際の変換は普通に行えることが分かりました。 せっかくなのでまとめて github に上げとこうか、どうしようか・・・ 数十行で終わりそうだが。 ただ、Stream のような遅延評価の Scala コレクションとかに対応させようとすると面倒そう。 誰かやってくれないかなぁ。

関数型オブジェクト指向AI プログラミング―Scala による人工知能の実装

関数型オブジェクト指向AI プログラミング―Scala による人工知能の実装

*1:別に List しかいらないなら JsonSerializer とまとめて1つのクラスに実装しておけば、GsonBuilder に1度の登録で済みます。