我有一些来自外部API的JSON,我无法控制。部分JSON的格式如下:
{
"room_0": {
"area_sq_ft": 151.2
},
"room_1": {
"area_sq_ft": 200.0
}
}
他们使用room_n作为n个元素的键,而不是使用他们应该拥有的数组。我没有使用room_0,room_1,room_2等创建案例类,而是将其转换为Seq [Room],这是我的Room case类:
case class Room(area: Double)
我使用来自play.api.libs.json
的Read来将JSON的其他部分转换为case类,并且更喜欢使用Reads进行此转换。我怎么能做到这一点?
这是我尝试过的。
val sqFtReads = (__ \ "size_sq_ft").read[Double]
val roomReads = (__ \ "size_sq_ft").read[Seq[Room]](sqFtReads).map(Room)
cmd19.sc:1: overloaded method value read with alternatives:
(t: Seq[$sess.cmd17.Room])play.api.libs.json.Reads[Seq[$sess.cmd17.Room]] <and>
(implicit r: play.api.libs.json.Reads[Seq[$sess.cmd17.Room]])play.api.libs.json.Reads[Seq[$sess.cmd17.Room]]
cannot be applied to (play.api.libs.json.Reads[Double])
val roomReads = (__ \ "size_sq_ft").read[Seq[Room]](sqFtReads).map(Room)
答案 0 :(得分:1)
一个棘手的小挑战,但Reads
完全可以实现。
首先,Reads[Room]
- 即单个Room
实例的转换器:
val roomReads = new Reads[Room] {
override def reads(json: JsValue): JsResult[Room] = {
(json \ "area_sq_ft").validate[Double].map(Room(_))
}
}
非常简单;我们查看JSON并尝试找到一个名为area_sq_ft
的顶级字段,该字段验证为Double
。如果一切顺利,我们会根据需要返回填充的Room
实例。
接下来,上游对象的转换器以良好的Postel's Law方式为您自己的消费者进行清理。
val strangeObjectReads = new Reads[Seq[Room]] {
override def reads(json: JsValue): JsResult[Seq[Room]] = {
json.validate[JsObject].map { jso =>
val roomsSortedNumerically = jso.fields.sortBy { case (name, contents) =>
val numericPartOfRoomName = name.dropWhile(!_.isDigit)
numericPartOfRoomName.toInt
}
roomsSortedNumerically.map { case (name, contents) =>
contents.as[Room](roomReads)
}
}
}
}
这里的关键是整个地段的json.validate[JsObject]
。通过map
ping这个,我们得到了我们需要包装整个事物的JsResult
,此外,我们可以访问JSON对象中的fields
,它被定义为{ {1}}。
为了确保我们在输出序列中按正确顺序放置字段,我们进行一些字符串操作,获取Seq[(String, JsValue)]
字符串的数字部分,并将其用作room_1
标准。我在这里有点天真,并假设您的上游服务器不会做任何令人讨厌的事情,比如跳过房间号码!
一旦您以数字方式对房间进行排序,我们就可以sortBy
对它们进行排序,并使用我们的map
转换器对每个房间进行转换。
您可能已经注意到我的自定义roomReads
实施绝对是不单行。这来自于处理古怪的上游JSON格式的痛苦经历。有点冗长,当上游服务器突然改变其JSON格式时,使用更多的变量并且稍微分解一些东西可以节省大量时间!