petitviolet blog

    Akka-HTTP - spray-jsonの使い方とAkka-HTTP

    2016-06-02

    QiitaScalaAkka-HTTP

    Akka-HTTP で公式にサポートされている JSON ライブラリのspray-jsonの使い方。 JSON Support — Akka Documentation Akka-HTTP と一緒に使いたいので、Marshaller/Unmarshaller として使用法も載せる。

    tl;dr

    • クラスのフィールドに独自の型を使わない場合 or 独自の型に対応させて json を入れ子にする場合
      • jsonFormatNを使えば手軽で良い
    • クラスのフィールドと json のフォーマットを変えたい場合
      • RootJsonProtocolを一生懸命実装する
    • Akka-HTTP ではSprayJsonSupportを extends すれば implicit にやってくれる - akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport

    基本的な使い方

    case class を json に変換、再変換を行う。

    case class User(id: Long, name: String)
    

    この case class に対応するRootJsonFormatを用意すればよい。 DefaultJsonProtocolを extends した object 内で実装するとこうなる。

    object UserJsonProtocol extends DefaultJsonProtocol {
      implicit val format = jsonFormat2(User.apply)
    }
    

    これを使って json <-> case class をするとこんな感じ。

    import UserJsonProtocol._
    
    // case class -> json
    val user = User(100, "alice")
    user.toJson  // {"id":100,"name":"alice"}
    
    // json -> case class
    val json = "{\"id\":10,\"name\":\"bob\"}"
    json.parseJson.convertTo[User]  // User(10,bob)
    

    ここで使ったjsonFormat2Userクラスのコンストラクタ引数が 2 つであることに対応している jsonFormatNを使用するとフィールド名(コンストラクタの仮引数名)が json の key となる。 フィールド名と json の key を変えたい場合はjsonFormatを使う。

    implicit val format: RootJsonFormat[User] = jsonFormat(User.apply, "user_id", "user_name")
    

    こう書くとコンストラクタに対して順番にuser_id, user_nameを割り当てることとなる。 つまり、この様に json <-> case class の関係が変化する。

    // case class -> json
    val user = User(100, "alice")
    user.toJson  // {"user_id":100,"user_name":"alice"}
    
    // json -> case class
    val json = "{\"user_id\":10,\"user_name\":\"bob\"}"
    json.parseJson.convertTo[User]  // User(10,bob)
    

    akka-http で使う

    akka-http と spray-json を組み合わせる。 具体的にはToResponseMarshallerとして spray-json を使うこととなる。 completeにクラスTのオブジェクトを渡して json として返却したい場合、 implicit なRootJsonProtocol[T]をスコープ内に用意しておけば良い。 変換が case class から json への一方向のみの場合、RootJsonWriterだけでも可。 Marshalling/Unmarshalling と json<->case class の対応関係は以下。

    • json -> case class が Unmarshalling
      • RootJsonReaderが必要
    • case class -> json が Marshalling
      • RootJsonWriterが必要

    controller 的なところにSprayJsonSupportを extends しておけば implicit にやってくれる。

    import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
    
    // jsonで返したい型
    case class User(id: Long, name: String)
    
    // implicitなRootJsonProtocol[User]を用意
    implicit val format: RootJsonProtocol[User] = jsonFormat2(User.apply)
    
    val route =
      // GET /user/<name>
      path("user" / ".+".r) { name =>
        val id: Long = new Random().nextInt(100).toLong
        val user: User = User(id, name)
    
        complete(user)
      }
    

    リクエストとレスポンスはこのようになる。 Content-Type: application/jsonとなっており、body も json になっている。

    $ curl localhost:8080/user/alice -i
    HTTP/1.1 200 OK
    Server: akka-http/2.4.2
    Date: Tue, 03 May 2016 10:30:40 GMT
    Content-Type: application/json
    Content-Length: 33
    
    {
      "id": 90,
      "name": "alice"
    }
    

    やや複雑な例

    前述の例ではクラスのフィールドの型がいわゆるプリミティブ型だけのものであった。 次は複雑なオブジェクト、クラスのフィールドの型が独自クラスになっているような場合を考える。

    クラス定義

    例として使うクラス定義を以下に示す。

    // IDにまつわる型
    trait Identifier[+A] extends Any {
      def value: A
    }
    object Identifier {
      type IdType = String
    }
    case class ID[A <: Entity[_]](value: IdType) extends AnyVal with Identifier[IdType]
    
    // Userの親となるEntity
    trait Entity[Id <: Identifier[_]] {
      val id: Id
    }
    
    // Userのフィールド型
    case class Name(value: String) extends AnyVal
    case class Email(value: String) extends AnyVal
    
    case class User(id: ID[User], name: Name, email: Email) extends Entity[ID[User]]
    

    単純な RootJsonProtocol を実装

    定義したUserクラスを json に変換するためのRootJsonProtocolを実装する。 RootJsonProtocol[User]を実装するためには、各フィールドの型に応じたRootJsonProtocolが必要となる。

    object UserJsonProtocol extends DefaultJsonProtocol {
      implicit val userIdFormat = jsonFormat1(ID.apply[User])
      implicit val nameFormat = jsonFormat1(Name.apply)
      implicit val emailFormat = jsonFormat1(Email.apply)
      implicit val userFormat = jsonFormat3(User.apply)
    }
    

    このRootJsonProtocol群を使ってUserクラスのオブジェクトを json に変換してみる。

    import UserJsonProtocol._
    val user = User(ID("alice-id"), Name("alice"), Email("alice@example.com"))
    user.toJson
    

    結果として以下の様な json が得られる。

    {
      "id": {
        "value": "alice-id"
      },
      "name": {
        "value": "alice"
      },
      "email": {
        "value": "alice@example.com"
      }
    }
    

    入れ子になってしまっているが、オブジェクトをクラス定義に沿って正しく serialize していると言える。

    akka-http での使用

    上記のjsonFormatNを使ったRootJsonFormatの実装だと、なぜか akka-http ではうまくいかず以下の様なエラーメッセージが出力されてレスポンスは返ってこない。

    Caused by: java.lang.RuntimeException: Cannot automatically determine case class field names and order for 'net.petitviolet.domain.user.Name', please use the 'jsonFormat' overload with explicit field name specification

    メッセージにある通り、明示的にjsonFormatを使う。

    object UserJsonProtocol extends DefaultJsonProtocol {
      implicit val userIdFormat = jsonFormat(ID.apply[User] _, "value")
      implicit val nameFormat = jsonFormat(Name.apply _, "value")
      implicit val emailFormat = jsonFormat(Email.apply _, "value")
      implicit val userFormat = jsonFormat(User.apply, "id", "name", "email")
    }
    

    これで上と同様な json が返ってくるようになる。

    フラットな json を作る

    入れ子になったクラスを serialize すると入れ子になった json が返ってきた。 入れ子になったvalueが必要でない場合、jsonFormatNを使った実装では実現できない。 そのため、RootJsonProtocol[User]を自前で実装することとなる。

    object UserJsonProtocol extends DefaultJsonProtocol {
      implicit val userFormat: RootJsonFormat[User] = new RootJsonFormat[User] {
        override def read(json: JsValue): User =
          json.asJsObject.getFields("id", "name", "email") match {
            case Seq(JsString(id), JsString(name), JsString(email)) =>
              User(ID(id), Name(name), Email(email))
            case _ => throw new DeserializationException("User")
          }
    
        override def write(user: User): JsValue = JsObject(
          "id" -> JsString(user.id.value),
          "name" -> JsString(user.name.value),
          "email" -> JsString(user.email.value)
        )
      }
    }
    

    これを使ってUserを json に変換するとフラットな json が得られる。

    {
      "id": "alice-id",
      "name": "alice",
      "email": "alice@example.com"
    }
    

    from: https://qiita.com/petitviolet/items/79f2bd3b4f1d54d38db1