【问题标题】:How to extract my case class from Json using Json4s?如何使用 Json4s 从 Json 中提取我的案例类?
【发布时间】:2018-09-27 12:10:56
【问题描述】:

在 akka scala 应用程序中,我使用了一个休息端点。因此,我想将其响应映射到案例类,但我还想通过转换某些属性来简化使用这些案例类,例如那些包含日期的。

所以给定一个 Json:

{
    "id": "20180213165959sCdJr",
    "createdAt": "2018-02-13T16:59:59.570+0000",
    "finishedAt": "2018-02-13T17:00:18.118+0000"
}

我想用它来创建这样一个类:

case class FinishedRun
(
  id: String,
  createdAt: Date,
  finishedAt: Date
)

我创建了这个构造器:

object FinishedRun {

  def apply(id: String,
            createdAt: String,
            finishedAt: String
           ): FinishedRun = {

    val getDate = (jsonValue: String) => {
      val format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
      format.parse(jsonValue)
    }


    new FinishedRun(id, createdAt = getDate(createdAt), finishedAt = getDate(finishedAt))
  }
}

虽然这适用于从头开始初始化案例类,但我在 json4s 库的帮助下通过 parse(json).as[FinishedRun] 方法提取此案例类时遇到了麻烦。

似乎json4s没有调用case类的构造函数,因此无法提取它,抛出:

No usable value for createdAt
Invalid date '2018-02-13T16:59:59.570+0000'
org.json4s.package$MappingException: No usable value for createdAt
Invalid date '2018-02-13T16:59:59.570+0000'
    at org.json4s.reflect.package$.fail(package.scala:95)

让 Json4s 正确解析日期我缺少什么?


这是我的测试用例:

import org.json4s._
import org.json4s.native.JsonMethods._
import org.scalatest.FlatSpec


class MarshallingTest extends FlatSpec {
  implicit val formats = DefaultFormats

  it should "marshall json object with date iso strings into a case class with Date properties" in {
    val json =
      """
        |{
        |    "id": "20180213165959sCdJr",
        |    "createdAt": "2018-02-13T16:59:59.570+0000",
        |    "finishedAt": "2018-02-13T17:00:18.118+0000"
        |}
      """.stripMargin

    val expected = FinishedRun(
      id = "20180213165959sCdJr",
      createdAt = "2018-02-13T16:59:59.570+0000",
      finishedAt = "2018-02-13T17:00:18.118+0000"
    )
    val actual = parse(json).extract[FinishedRun]

    assert(actual == expected)
  }
}

【问题讨论】:

    标签: json scala unmarshalling case-class json4s


    【解决方案1】:

    您需要定义您的CustomSerializer(1)CustomSerializer(2)。我将日期类型从Date 更改为ZonedDateTime

    import java.time.ZonedDateTime
    import java.time.format.DateTimeFormatter
    
    import org.json4s._
    import org.json4s.JsonAST._
    import org.json4s.JsonDSL._
    import org.json4s.native.JsonMethods._
    import org.scalatest.FlatSpec
    
    case class FinishedRun
    (
      id: String,
      createdAt: ZonedDateTime,
      finishedAt: ZonedDateTime
    )
    
    object FinishedRunSerializer {
      val dateTimeFmt = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
    }
    
    class FinishedRunSerializer extends CustomSerializer[FinishedRun](
      format => ( {
        case jObj: JObject =>
          implicit val fmt = format
          val id = (jObj \ "id").extract[String]
          val created = ZonedDateTime.parse((jObj \ "createdAt").extract[String],
            FinishedRunSerializer.dateTimeFmt)
          val finished = ZonedDateTime.parse((jObj \ "finishedAt").extract[String],
            FinishedRunSerializer.dateTimeFmt)
          FinishedRun(id, created, finished)
      }, {
        case finishedRun: FinishedRun =>
          ("id" -> finishedRun.id) ~
            ("createdAt" -> finishedRun.createdAt.format(FinishedRunSerializer.dateTimeFmt)) ~
            ("finishedAt" -> finishedRun.finishedAt.format(FinishedRunSerializer.dateTimeFmt))
      }
      ))
    

    在你测试或使用的地方别忘了带上FinishedRunSerializer:

    implicit val formats = DefaultFormats + new FinishedRunSerializer()
    

    【讨论】:

    • 此答案有效,但它暗示 JSON 已正确排序。如果有一个 json 对象,其中 id 最后出现而不是第一个出现,这会中断。此外,它需要更改为ZonedDateTime
    • 我已经更新了代码。它不依赖于顺序。如果您想了解有关时区的信息,ZonedDateTime 是正确的类型。
    【解决方案2】:

    解决您的问题的一个简单方法可能是使用 org.json4s 中的 序列化 trait。您应该能够执行以下操作:

    val finishedRun = read[FinishedRun](json)
    

    详情和示例请参考此链接:https://github.com/json4s/json4s#serializing-polymorphic-lists

    【讨论】: