🥰

Scala の http4s で Json を扱う例

2022/10/15に公開

コード

Server.scala
//> using scala "3.2.0"
//> using lib "org.typelevel::cats-core:2.8.0"
//> using lib "org.typelevel::cats-effect:3.3.14"

//> using lib "org.http4s::http4s-core:1.0.0-M37"
//> using lib "org.http4s::http4s-dsl:1.0.0-M37"
//> using lib "org.http4s::http4s-ember-server:1.0.0-M37"
//> using lib "org.http4s::http4s-circe:1.0.0-M37"

//> using lib "io.circe::circe-core:0.15.0-M1"
//> using lib "io.circe::circe-generic:0.15.0-M1"


import cats.effect._
import cats.syntax.all._

import com.comcast.ip4s._
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.implicits._
import org.http4s.ember.server._

import io.circe._
import io.circe.syntax._
import org.http4s.circe.CirceEntityCodec._

case class Payload(
    foo: Int,
    bar: String,
    buzz: Buzz
) derives Decoder,
      Encoder.AsObject
case class Buzz(
    hoge: Double,
    fuga: Option[Int],
    piyo: String
)

object Main extends IOApp:
  val extractBuzzHoge = (p: Payload) => Ok(p.buzz.hoge.asJson)

  val example = HttpRoutes
    .of[IO] { case req @ POST -> Root / "example" =>
      req.as[Payload] >>= extractBuzzHoge
    }
    .orNotFound

  def run(args: List[String]): IO[ExitCode] = EmberServerBuilder
    .default[IO]
    .withHost(ipv4"0.0.0.0")
    .withPort(port"8080")
    .withHttpApp(example)
    .build
    .useForever
    .as(ExitCode.Success)

scala-cli Server.scala

解説

利用するライブラリ

//> using scala "3.2.0"
//> using lib "org.typelevel::cats-core:2.8.0"
//> using lib "org.typelevel::cats-effect:3.3.14"

//> using lib "org.http4s::http4s-core:1.0.0-M37"
//> using lib "org.http4s::http4s-dsl:1.0.0-M37"
//> using lib "org.http4s::http4s-ember-server:1.0.0-M37"
//> using lib "org.http4s::http4s-circe:1.0.0-M37"

//> using lib "io.circe::circe-core:0.15.0-M1"
//> using lib "io.circe::circe-generic:0.15.0-M1"

ここでは Scala のバージョンや利用するライブラリを指定している.
cats-effect は非同期ランタイム、 http4s は Scala で http を扱うライブラリ、circe は Scala で json を扱うためのライブラリである.

sbt プロジェクトで利用する場合は以下のようになる.

build.sbt
lazy val server = project.in(file("."))
  .settings(
    scalaVersion := "3.2.0",
    libraryDependencies ++= Seq(
      "org.typelevel" %% "cats-core" % "2.8.0",
      "org.typelevel" %% "cats-effect" % "3.3.14",
      "org.http4s" %% "http4s-core" % "1.0.0-M37",
      "org.http4s" %% "http4s-dsl" % "1.0.0-M37",
      "org.http4s" %% "http4s-ember-server" % "1.0.0-M37",
      "org.http4s" %% "http4s-ember-circe" % "1.0.0-M37",
      "io.circe" %% "circe-core" % "0.15.0-M1",
      "io.circe" %% "circe-generic" % "0.15.0-M1",
    ) 
  )

また、以下のインポートをすることで、型が circe の Encoder 型クラスを持っていれば io.circe.json から、http の Response へよしなに変換してくれる.

import org.http4s.circe.CirceEntityCodec._

↓ で Json から Response に変換されている.

object Main extends IOApp:
  val extractBuzzHoge = (p: Payload) => Ok(p.buzz.hoge.asJson)

利用する Json に対応する型を定義する

以下のような json に対応する Payload 型とBuzz 型を定義する.

example.json
{
  "foo" : 1,
  "bar" : "....",
  "buzz" : {
    "hoge" : 1.0,
    "fuga" : null,
    "piyo" "piyopiyo"
  }
}
case class Payload(
    foo: Int,
    bar: String,
    buzz: Buzz
) derives Decoder,
      Encoder.AsObject
case class Buzz(
    hoge: Double,
    fuga: Option[Int],
    piyo: String
)

Payloadderives 節で io.circe.Decoder, io.circe.Encoder.AsObject の型クラスを導出している. import io.circe.syntax._ とすることで、Payload 型のクラスを asJsonio.circe.Json にしたり、io.circe.Json から Payload 型に変換できるようになる.

ルーティング

/example example.json のような json を受け取って、payload.buzz.hoge を取り出して返すエンドポイントは以下のように書ける.

object Main extends IOApp:
  val extractBuzzHoge = (p: Payload) => Ok(p.buzz.hoge.asJson)
  val example = HttpRoutes
    .of[IO] { case req @ POST -> Root / "example" =>
      req.as[Payload] >>= extractBuzzHoge
    }
    .orNotFound

最後に、http サーバーに example サービスをバインディングしてサーバーを起動している.

def run(args: List[String]): IO[ExitCode] = EmberServerBuilder
  .default[IO]
  .withHost(ipv4"0.0.0.0")
  .withPort(port"8080")
  .withHttpApp(example)
  .build
  .useForever
  .as(ExitCode.Success)

Discussion