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アプリケーション開発における信頼性の高いデータ検証を実現します。