📝
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 流儀
-
Action(parse.json)
受信ペイロードを JSON として扱い、他フォーマットなら 415 を返す。 -
validate[T]
implicit Reads[T]
が見つかるとJsSuccess(value)
/JsError(errors)
に自動振り分け。 -
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. さらに堅牢にするアイデア
- 独自の Reads
val emailReads: Reads[String] = Reads.pattern("^[^@]+@[^@]+\\.[^@]+$".r, "error.email")
- BodyParsers.tolerantJson で サイズ上限 をかける
- 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