In Scala there are quite a few frameworks that make the implementation of RESTful JSON services quite straightforward (Play, Scalatra, Akka HTTP are the main ones). They all follow the same general idea: a JSON payload deserialize into a Scala class (usually a case class) and serialize back into JSON. For example a JSON payload like:
{"name": "Paul", "age": 45, "gender": "male"}
Can be easily deserialized into something like:
final case class User(name: String, age: Int, gender: String)
This is pretty standard stuff and works almost out of the box in all the mentioned framework but in some cases you might want to take advantage of the type system for example by defining a type for “age”:
// A value class representing a person's age final class Age(val age: Int) extends AnyVal
And also constant values for “gender” rather than a generic String
type:
sealed trait Gender { val sex: String } final case object Male extends Gender { val sex = "male" } final case object Female extends Gender { val sex = "female" } final case object Other extends Gender { val sex = "other" } object GenderHelper { private val genders: Set[Gender] = Set(Male, Female, Other) def toGender(sex: String): Option[Gender] = genders.find(_.sex == sex) }
We can now define User
in a more robust way as:
final case class User(name: String, age: Age, gender: Gender)
Which looks better but doesn’t work out of the box anymore as, regardless of the JSON scala library we use, there is no way to tell how to serialize/deserialize types like Age
or Gender
. We need to write our own JSON custom serializer/deserializer. The following example uses Akka HTTP and Json4s (code is available here).
We first need to write two serializer/deserializer. One for Age
:
import com.lansalo.model.Age import org.json4s.CustomSerializer import org.json4s.JsonAST.JInt case object AgeSerializer extends CustomSerializer[Age](format => ( { case JInt(age) => new Age(age.intValue) }, { case age: Int => JInt(BigInt(age)) }))
And one for Gender
:
import com.lansalo.model.{Gender, GenderHelper} import org.json4s.CustomSerializer import org.json4s.JsonAST.JString case object GenderSerializer extends CustomSerializer[Gender](format => ( { case JString(gender) => GenderHelper.toGender(gender).get }, { case gender: Gender => JString(gender.sex) }))
Then we just need to add them to the default json4s formats (in a Trait here):
import com.lansalo.json.serializer.{AgeSerializer, GenderSerializer} import org.json4s.DefaultFormats trait JsonSupport { implicit val formats = DefaultFormats + GenderSerializer + AgeSerializer }
And finally bring the json formats in scope where we need it:
import akka.actor.ActorSystem import akka.event.Logging import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.directives.MethodDirectives.{get, post} import akka.http.scaladsl.server.directives.PathDirectives.path import akka.http.scaladsl.server.directives.RouteDirectives.complete import akka.util.Timeout import com.lansalo.json.JsonSupport import com.lansalo.model.{Age, Female, User} import de.heikoseeberger.akkahttpjson4s.Json4sSupport import org.json4s.jackson import scala.concurrent.duration._ trait UserRoutes extends JsonSupport { implicit def system: ActorSystem lazy val log = Logging(system, classOf[UserRoutes]) implicit lazy val timeout = Timeout(5.seconds) import Json4sSupport._ implicit val serialization = jackson.Serialization // or native.Serialization lazy val userRoutes: Route = pathPrefix("users") { pathEnd { post { entity(as[User]) { user => log.info(s"Received user: $user") complete((StatusCodes.Created, "OK")) } } } ~ path(Segment) { name => get { complete(User(name, new Age(22), Female)) } } } }