Play JSON Validation
Play FrameworkのためのJSON検証ライブラリ
概要
Play JSON Validationは、Play Frameworkに組み込まれた強力なJSON検証ライブラリです。Scalaの型システムを活用し、JSON データの読み取り、書き込み、検証を型安全に行うための包括的な機能を提供します。関数型プログラミングの原則に基づいて設計されており、コンビネータパターンを使用して複雑な検証ルールを構築できます。
主な特徴
- 型安全なJSON処理: Scalaの型システムを活用した安全なJSON操作
- 双方向変換: ReadsとWritesによるJSONとScalaオブジェクト間の相互変換
- 柔軟な検証: カスタムバリデータによる複雑な検証ルールの実装
- エラー処理: 詳細なエラー情報を含む検証結果
- Play Framework統合: シームレスなフレームワーク統合
- 関数型アプローチ: コンビネータを使用した検証ルールの合成
インストール
SBT
libraryDependencies += "com.typesafe.play" %% "play-json" % "2.10.0"
Maven
<dependency>
<groupId>com.typesafe.play</groupId>
<artifactId>play-json_2.13</artifactId>
<version>2.10.0</version>
</dependency>
基本的な使い方
Readsの定義
import play.api.libs.json._
import play.api.libs.functional.syntax._
case class User(name: String, age: Int, email: String)
implicit val userReads: Reads[User] = (
(JsPath \ "name").read[String] and
(JsPath \ "age").read[Int] and
(JsPath \ "email").read[String]
)(User.apply _)
Writesの定義
implicit val userWrites: Writes[User] = (
(JsPath \ "name").write[String] and
(JsPath \ "age").write[Int] and
(JsPath \ "email").write[String]
)(unlift(User.unapply))
Formatの定義(ReadsとWritesの組み合わせ)
implicit val userFormat: Format[User] = Json.format[User]
検証ルール
基本的な検証
val nameReads: Reads[String] = (JsPath \ "name").read[String](minLength[String](2))
val ageReads: Reads[Int] = (JsPath \ "age").read[Int](min(0) and max(120))
val emailReads: Reads[String] = (JsPath \ "email").read[String](email)
カスタムバリデータ
import play.api.libs.json.Reads._
def uniqueUsername(existingUsers: Set[String]): Reads[String] =
Reads.StringReads.filter(JsonValidationError("Username already exists"))(
username => !existingUsers.contains(username)
)
val usernameReads: Reads[String] = (JsPath \ "username").read[String](
minLength[String](3) and uniqueUsername(Set("admin", "user"))
)
複雑な検証ルール
case class Address(street: String, city: String, zipCode: String)
case class Person(
name: String,
age: Int,
email: String,
address: Address,
phoneNumbers: List[String]
)
implicit val addressReads: Reads[Address] = (
(JsPath \ "street").read[String](minLength[String](1)) and
(JsPath \ "city").read[String](minLength[String](1)) and
(JsPath \ "zipCode").read[String](pattern("\\d{3}-\\d{4}".r))
)(Address.apply _)
implicit val personReads: Reads[Person] = (
(JsPath \ "name").read[String](minLength[String](2)) and
(JsPath \ "age").read[Int](min(0) and max(120)) and
(JsPath \ "email").read[String](email) and
(JsPath \ "address").read[Address] and
(JsPath \ "phoneNumbers").read[List[String]](minLength[List[String]](1))
)(Person.apply _)
エラー処理
検証結果の処理
val json = Json.parse("""
{
"name": "John Doe",
"age": 30,
"email": "[email protected]"
}
""")
json.validate[User] match {
case JsSuccess(user, _) =>
println(s"Valid user: $user")
case JsError(errors) =>
println(s"Validation errors: ${JsError.toJson(errors)}")
}
エラーメッセージのカスタマイズ
val customEmailReads: Reads[String] =
Reads.email.filterNot(JsonValidationError("Invalid email format"))(_.isEmpty)
val detailedUserReads: Reads[User] = (
(JsPath \ "name").read[String]
.filter(JsonValidationError("Name must be at least 2 characters"))(_.length >= 2) and
(JsPath \ "age").read[Int]
.filter(JsonValidationError("Age must be between 0 and 120"))(age => age >= 0 && age <= 120) and
(JsPath \ "email").read[String](customEmailReads)
)(User.apply _)
Play Frameworkとの統合
コントローラーでの使用
import play.api.mvc._
import play.api.libs.json._
import javax.inject._
@Singleton
class UserController @Inject()(cc: ControllerComponents) extends AbstractController(cc) {
def createUser: Action[JsValue] = Action(parse.json) { request =>
request.body.validate[User].fold(
errors => {
BadRequest(Json.obj("errors" -> JsError.toJson(errors)))
},
user => {
// ユーザーを保存
Ok(Json.obj("message" -> s"User ${user.name} created"))
}
)
}
}
フォーム検証との統合
import play.api.data._
import play.api.data.Forms._
case class UserForm(name: String, age: Int, email: String)
val userForm: Form[UserForm] = Form(
mapping(
"name" -> nonEmptyText(minLength = 2),
"age" -> number(min = 0, max = 120),
"email" -> email
)(UserForm.apply)(UserForm.unapply)
)
// JSONからフォームへの変換
def jsonToForm(json: JsValue): Form[UserForm] = {
userForm.bind(json)
}
高度な使用例
条件付き検証
sealed trait PaymentMethod
case class CreditCard(number: String, cvv: String) extends PaymentMethod
case class BankTransfer(accountNumber: String) extends PaymentMethod
val paymentMethodReads: Reads[PaymentMethod] =
(JsPath \ "type").read[String].flatMap {
case "credit_card" =>
((JsPath \ "number").read[String](minLength[String](16)) and
(JsPath \ "cvv").read[String](pattern("\\d{3,4}".r))
)(CreditCard.apply _)
case "bank_transfer" =>
(JsPath \ "accountNumber").read[String](minLength[String](10))
.map(BankTransfer.apply)
case other =>
Reads.failed[PaymentMethod](s"Unknown payment method: $other")
}
再帰的データ構造の検証
case class TreeNode(value: Int, children: List[TreeNode])
implicit lazy val treeNodeReads: Reads[TreeNode] = (
(JsPath \ "value").read[Int] and
(JsPath \ "children").lazyRead(Reads.list[TreeNode](treeNodeReads))
)(TreeNode.apply _)
トランスフォーマー
val userTransformer: Reads[JsObject] = (
(JsPath \ "name").json.pickBranch and
(JsPath \ "age").json.pickBranch and
(JsPath \ "email").json.pickBranch and
(JsPath \ "createdAt").json.put(JsString(new Date().toString))
).reduce
val transformedJson = json.transform(userTransformer)
ベストプラクティス
1. 明示的な型定義
// 良い例
implicit val userReads: Reads[User] = Json.reads[User]
// 避けるべき例
implicit val userReads = Json.reads[User] // 型が推論される
2. エラーメッセージの国際化
import play.api.i18n.Messages
def localizedError(key: String)(implicit messages: Messages): JsonValidationError =
JsonValidationError(messages(key))
val i18nUserReads: Reads[User] = (
(JsPath \ "name").read[String]
.filter(localizedError("error.name.tooShort"))(_.length >= 2) and
(JsPath \ "age").read[Int]
.filter(localizedError("error.age.invalid"))(age => age >= 0 && age <= 120) and
(JsPath \ "email").read[String](email)
)(User.apply _)
3. 検証の再利用
object ValidationRules {
val validName: Reads[String] = minLength[String](2) and maxLength[String](50)
val validAge: Reads[Int] = min(0) and max(120)
val validEmail: Reads[String] = email
def uniqueField[T](existingValues: Set[T]): Reads[T] =
Reads.filter[T](JsonValidationError("Value already exists"))(
value => !existingValues.contains(value)
)
}
パフォーマンスの最適化
遅延評価の活用
// 大きなJSONの部分的な検証
val partialReads: Reads[String] = (JsPath \ "data" \ "user" \ "name").read[String]
// 必要な部分だけを抽出
json.validate[String](partialReads) match {
case JsSuccess(name, _) => println(s"User name: $name")
case JsError(_) => println("Name not found")
}
ストリーミング処理
import play.api.libs.json.JsonParser
import akka.stream.scaladsl._
val jsonSource: Source[ByteString, _] = // JSON データソース
jsonSource
.via(JsonParser.flow)
.collect {
case JsObject(fields) if fields.contains("user") => fields("user")
}
.runForeach { userJson =>
userJson.validate[User].foreach(processUser)
}
テスト
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import play.api.libs.json._
class UserValidationSpec extends AnyFlatSpec with Matchers {
"User validation" should "accept valid JSON" in {
val validJson = Json.obj(
"name" -> "John Doe",
"age" -> 30,
"email" -> "[email protected]"
)
validJson.validate[User] should be a JsSuccess[User]
}
it should "reject invalid email" in {
val invalidJson = Json.obj(
"name" -> "John Doe",
"age" -> 30,
"email" -> "invalid-email"
)
validJson.validate[User] should be a JsError
}
}
まとめ
Play JSON Validationは、Scalaアプリケーションにおける堅牢なJSON処理と検証を可能にする優れたライブラリです。型安全性、関数型プログラミングの原則、Play Frameworkとの緊密な統合により、Webアプリケーション開発における信頼性の高いデータ検証を実現します。