Simple Scala: Implicit JSON conversion with Scala

简介

本文将介绍Json格式的打包和解包。

Json扩展库

在Scala中又大量的Json库可供选择。很大程度上取决于项目框架的选择和项目配置。本文不打算对现有工具包中的最佳选择进行辩论。
下面的文章将主要介绍Argonaut,但是大概的方法也同样适用于其他Json库。我选择Argonaut是因为它很适合我的功能发展,同时和Scala还有Scalatra都配合的很好。
其他Json库还有Json4s、Spray-Json、Lift-Json等,其中Json4s支持Native和Jackson。

转换

在我们讨论转换之前首先需要定义数据模型,这里使用slick sample structuree:

case class Person (id: Int,name: String,age: Int,addressId:Int)
case class Address (id: Int, street: String,city: String)

// no direct reference, to fit with slick database models
case class PersonWithAddress(person: Person, address: Address)

现在可以实例化一个带有address的person:

val person = Person(0, "John Rambo" , 67, 0)
val address = Address(0, "101 W Main St", "Madison, Kentucky")
val pa = PersonWithAddress(person, address)

如果是在开发REST服务的话,需要适用json格式来接受或响应数据。如果需要手动构造每一个路由的数据结构转换将会非常恐怖,因此我们需要一些类似下面的功能:

// convert the person to json
val json = pa.asJson

Scala内部提供了隐式类型转换的功能。我们的情况使用这种功能会非常方便,并配合Argonaut的编解码器。对于我们的情况则有如下定义:

// implicit conversion with argonaut
  implicit def PersonAddressEncodeJson: EncodeJson[PersonWithAddress] =
  EncodeJson((p: PersonWithAddress) =>
    ("id" := p.person.id) ->:
    ("name" := p.person.name) ->:
    ("age" := p.person.age) ->:
    ("address" := Json (
      ("id" := p.address.id),
      ("street" := p.address.street),
      ("city" := p.address.city)
    )
  ) ->: jEmptyObject)

现在我们把所有的步骤拼接起来后就可以完成从scala数据类型到Json的格式转化:

{
  "id": 0,
  "name": "John Rambo",
  "age": 67,
  "address": {
    "id": 0,
    "street": "101 W Main St",
    "city": "Madison, Kentucky"
  }
}

要解析Json数据:

implicit def PersonAddressDecodeJson: DecodeJson[PersonWithAddress] =
  DecodeJson(c => for {

    id <- (c --\ "id").as[Int]
    name <- (c --\ "name").as[String]
    age <- (c --\ "age").as[Int]
    address <- (c --\ "address").as[Json]

    // extract data from address
    addressid <- (address.acursor --\ "id").as[Int]
    street <- (address.acursor --\ "street").as[String]
    city <- (address.acursor --\ "city").as[String]

  } yield PersonWithAddress(Person(id, name, age, addressid), Address(addressid, street, city)))

解析的结果得到我们期望的scala数据结构:

PersonWithAddress(Person(0,John Rambo,67,0),Address(0,101 W Main St,Madison, Kentucky))

相对于Node.js,这种转换也是一种验证,如果一个参数错误,则转换失败。如果想要使部分参数可选,则需要进行调整,使用Option[Int]替换Int:

age <- (c --\ "age").as[Option[Int]]

一个复杂的实例

import argonaut._, Argonaut._

object ImplicitConversion {

  // data model based on http://slick.typesafe.com/doc/2.1.0/orm-to-slick.html
  case class Person (id: Int,name: String,age: Int,addressId:Int)
  case class Address (id: Int, street: String,city: String)

  // no direct reference, to fit with slick database models
  case class PersonWithAddress(person: Person, address: Address)

  // implicit conversion with argonaut
  implicit def PersonAddressEncodeJson: EncodeJson[PersonWithAddress] =
  EncodeJson((p: PersonWithAddress) =>
    ("id" := p.person.id) ->:
    ("name" := p.person.name) ->:
    ("age" := p.person.age) ->:
    ("address" := Json (
      ("id" := p.address.id),
      ("street" := p.address.street),
      ("city" := p.address.city)
    )
  ) ->: jEmptyObject)

  implicit def PersonAddressDecodeJson: DecodeJson[PersonWithAddress] =
  DecodeJson(c => for {

    id <- (c --\ "id").as[Int]
    name <- (c --\ "name").as[String]
    age <- (c --\ "age").as[Int]
    address <- (c --\ "address").as[Json]

    // extract data from address
    addressid <- (address.acursor --\ "id").as[Int]
    street <- (address.acursor --\ "street").as[String]
    city <- (address.acursor --\ "city").as[String]

  } yield PersonWithAddress(Person(id, name, age, addressid), Address(addressid, street, city)))

  def main(args: Array[String]) {
    // running a sample
    val person = Person(0, "John Rambo" , 67, 0)
    val address = Address(0, "101 W Main St", "Madison, Kentucky")
    val pa = PersonWithAddress(person, address)

    // convert the person to json
    val json = pa.asJson
    var content = json.toString()

    println (content)

    // we should get a person instance here
    var padecoded : PersonWithAddress = content.decodeOption[PersonWithAddress].get

    println (padecoded)
  }
}

结论

简单的几行代码,我们就封装了Json的打包和拆包,真的是很容易。可能不像在Node中那么直观,但是带有附加的类型检查,如果是在Node中则会很复杂。

参考

Implicit JSON conversion with Scala - Christoph Hartmann