📝

Play Framework+Scala レシピ #4 ─ フォーム POST をバリデートして JSON レスポンス

に公開

0. なぜ “Reads” を使うのか?

Play にはフォームバインド用 Form と JSON 変換用 Reads/Writes があるけど、今回はどっち?

  • SPA やモバイルアプリ から呼ばれる API を想定
  • したがって クライアント ⇔ サーバ間は JSON が自然
  • JSON ⇒ Scala の case class へ安全に変換する仕組みが Reads

👉 Reads = 「型変換 + バリデーション」 を同時に行うプロセッサ

HelloForm モデルを作る

まずはデータ構造と検証ルールを 1 ファイルに閉じ込めます。

app/models/HelloForm.scala を作成します。

package models

import play.api.libs.json._
import play.api.libs.functional.syntax._

// クライアントから受け取りたいフィールド
case class HelloForm(name: String, age: Int)

object HelloForm {
    /**
    * JSON → HelloForm 変換 + バリデーション
    *   name: 1文字以上
    *   age : 0〜120 の整数
    */
    implicit val reads: Reads[HelloForm] = (
        (JsPath \ "name").read[String](Reads.minLength[String](1)) and
        (JsPath \ "age").read[Int](Reads.min(0) keepAnd Reads.max(120))
    )(HelloForm.apply _)
}
説明
(JsPath \ "name") JSON の "name" フィールドを掴む
.read[String](Reads.minLength(1)) 文字列に変換し かつ 1 文字未満なら失敗
(Reads.min(0) keepAnd Reads.max(120)) 両方満たす必要がある複合チェック

implicit にしておくと後で validate[HelloForm] が自動で見つける。

2. Controller で「受け取り->検証->レスポンス」三段構え

package controllers

import javax.inject._
import play.api.mvc._
import play.api.libs.json._
import models.HelloForm

@Singleton
class HelloApiController @Inject()(cc: ControllerComponents)
    extends AbstractController(cc) {

  /**
   * POST /api/hello
   * 受信JSONを HelloForm にバインド
   *   - 成功: 200 OK + message
   *   - 失敗: 400 BadRequest + error詳細
   */
  def hello: Action[JsValue] = Action(parse.json) { implicit request =>

    request.body.validate[HelloForm] match {

      case JsSuccess(form, _) =>
        // ビジネスロジック(今回は文字列整形するだけ)
        val msg = s"Hello, ${form.name} (${form.age})!"
        Ok(Json.obj("message" -> msg))

      case JsError(errors) =>
        // Play標準形式でエラー内容をJSON化
        BadRequest(Json.obj(
          "status"  -> "validation_error",
          "details" -> JsError.toJson(errors)
        ))
    }
  }
}

ここで押さえる 3 つの Play 流儀

  1. Action(parse.json)
    受信ペイロードを JSON として扱い、他フォーマットなら 415 を返す。
  2. validate[T]
    implicit Reads[T] が見つかると JsSuccess(value) / JsError(errors) に自動振り分け。
  3. Ok(...) / BadRequest(...)
    Play の Result は ステータス + ボディ をワンライナーで生成。
    JSON なら Json.obj(...), Json.toJson(caseClass) が便利。

3. routes に登録

POST  /api/hello   controllers.HelloApiController.hello

4. リクエスト例とレスポンス

まずは成功パターンを試してみましょう。

% curl -X POST http://localhost:9000/api/hello      -H "Content-Type: application/json"      -d '{"name":"Alice","age":23}'
{"message":"Hello, Alice (23)!"}
シナリオ リクエスト レスポンス
✅ 正常 {"name":"Alice","age":23} 200 OK
{"message":"Hello, Alice (23)!"}
❌ age が負数 {"name":"Alice","age":-1} 400 BadRequest
{"status":"validation_error","details":{"obj.age":[{"msg":["error.min"],"args":[0]}]}}
❌ name が空 {"name":"","age":30} 400 BadRequest (minLength で検出)

5. さらに堅牢にするアイデア

  1. 独自の Reads
    val emailReads: Reads[String] =
        Reads.pattern("^[^@]+@[^@]+\\.[^@]+$".r, "error.email")
    
  2. BodyParsers.tolerantJson で サイズ上限 をかける
  3. Action.async にして非同期 DB 呼び出しへ拡張

6. まとめ 🎯

学んだこと キーワード
JSON 受信 → case class 変換 Reads, validate[T]
バリデーション失敗を 400 で返す JsError, BadRequest
Play の「最短 API 実装手順」 Action(parse.json) + Ok(Json.obj(...))

この形をテンプレにすれば、どんなエンドポイントでも「スキーマ定義 → バリデーション → 200/400 JSON」の黄金パターンがすぐ作れます。

次回は データを DB (ScalikeJDBC) に保存し、ID 付きレスポンスを返す ステップに進みます。
Enjoy Play & Happy Coding! 📝✨

Discussion