Reads[A]
にはcomposeWith
メソッドがあるため、これを利用して余分なフィールドを検出できる。composeWith
は、いったん別のReads
を事前に経由させるメソッド。
Scala 3.3.0、play-json
3.0.4で確認した。
前提
まず、あるcase classと、余分なフィールドを含むJSON、そして余分なフィールドを含まないJSONがあるとする。
import play.api.libs.json._ case class User(name: String, age: Int) val json: JsValue = Json.parse(""" { "name" : "Windymelt", "age": 31, "like": "beer" } """) val exactJson: JsValue = Json.parse(""" { "name" : "Windymelt", "age": 31 } """)
素朴にReads
を作って普通にパースしようとすると以下のような形になる。
import play.api.libs.functional.syntax._ val userReadBuilder = (JsPath \ "name").read[String] and (JsPath \ "age").read[Int] implicit val userReads: Reads[User] = userReadBuilder.apply(User.apply) exactJson.as[User]
世の中の大抵のJSONパーサは、入力に対しては寛容なので、余分なフィールドを持つJSONを投入してもパースに成功する。
json.as[User]
厳格なReads[A]
まぁ大人の事情で厳格にパースしたいこともある。余分なフィールドを許さないReads
を構築するには、composeWith
が便利だ:
// userReadsとは別のスコープで implicit val exactUserRead = userReads.composeWith(Reads[JsObject] { case j: JsObject if (j.keys.filterNot(Set("name", "age").contains)).isEmpty => JsSuccess(j) case j: JsObject => JsError( s"extra fields: ${(j.keys.filterNot(Set("name", "age").contains))}" ) case _: JsValue => JsError("not an object") }) println(exactJson.as[User]) // ok println(json.as[User]) // fail
JsObject
のフィールドを検査し、問題ない場合にのみそのJsObject
を返す検査用Reads
を用意しているのがミソだ。composeWith
は成功か失敗かも制御できるので事前検査に応用できる。
フィールドを自動的に導出する
フィールドの導出はちょっと手がかかる。手でやるのが困る場合はマクロなどを利用するほかない。
Scala 3ではMirror
があるし、コンパイルタイムで計算ができるのでcase classのフィールドを持ってくるのは割と簡単だ:
import scala.deriving.Mirror import scala.compiletime.constValueTuple inline def labelsOf[A](using p: Mirror.ProductOf[A]) = constValueTuple[p.MirroredElemLabels] inline def labels = labelsOf[User].toList
Scala 2の場合は、たぶんリフレクションを使うか、厄介なマクロを利用しなければならない・・・(お茶を濁す)