Play Framework2.6のJsonで23要素以上のデシリアライズとシリアライズ


業務の都合上の話ですが、23要素以上のフラットな形式のJSONリクエストパラメータをうまくケースクラスにマッピングして当てはめる方法を少し調べました。
その時のメモです。
Scala/Play初心者のため、よい実装があったらぜひ教えてください。

検討ケースとしては以下です。

1. (Pattern1)パラメータが22個以下: Playのデファクトスタンダードな実装
2. (Pattern2)パラメータが23個以上: for-yieldを使ってケースクラスにマッピングする
3. (Pattern3)パラメータが23個以上: play-json-extensionを使ってケースクラスにマッピングする

※前提事項: 1.の実装の場合,23要素以上に対応できません。

**結果としてはパラメータ異常時のレスポンスが異なりました**。
以降は重要だと思われる箇所を**太字**にして記載しています。

なお、リクエストでもらったJSONのパラメータをケースクラスにマッピングした後にレスポンスで返すような単純な実装で試してみました。
コントローラは以下のようになっています。
`BadRequest(Json.obj("result" -> "failure", "error" -> JsError.toJson(error)))`でエラーの情報を返しています。

```TestParamsController.scala
package pattern

import javax.inject.{Inject, Singleton}
import play.api.libs.json._
import play.api.mvc.{AbstractController, Action, ControllerComponents}

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

  // pattern1
  // Jsonパラメータが22個の制約を受けない
  def pattern1: Action[JsValue] = Action(parse.json) { implicit request =>

    import Pattern1._

    request.body.validate[Pattern1].map { form =>
      Ok(Json.obj("result" -> Json.toJson(form)))
    }.recoverTotal { error =>
      BadRequest(Json.obj("result" -> "failure", "error" -> JsError.toJson(error)))
    }
  }

  // Pattern2
  // Jsonパラメータ22個の制約を突破する
  // for-yieldで実装
  def pattern2: Action[JsValue] = Action(parse.json) { implicit request =>

    import Pattern2._

    request.body.validate[Pattern2].map { form =>
      Ok(Json.obj("result" -> Json.toJson(form)))
    }.recoverTotal { error =>
      BadRequest(Json.obj("result" -> "failure", "error" -> JsError.toJson(error)))
    }
  }

  // Pattern3
  // Jsonパラメータ22個の制約を突破する。
  // play-json-extensionで実装
  def pattern3: Action[JsValue] = Action(parse.json) { implicit request =>

    import Pattern3._

    request.body.validate[Pattern3].map { form =>
      Ok(Json.obj("result" -> Json.toJson(form)))
    }.recoverTotal { error =>
      BadRequest(Json.obj("result" -> "failure", "error" -> JsError.toJson(error)))
    }
  }

}
```

ルーティングは以下のように定義しています。

```routes
POST    /pattern1                   pattern.TestParamsController.pattern1
POST    /pattern2                   pattern.TestParamsController.pattern2
POST    /pattern3                   pattern.TestParamsController.pattern3
```

`build.sbt`にはplay-json-extensionを追加しています。

```build.sbt
libraryDependencies += "ai.x" %% "play-json-extensions" % "0.10.0"
```

## (Pattern1)パラメータが22個以下: Playのデファクトスタンダードな実装

普通のパターンです。

### 実装

```Pattern1.scala
package pattern

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

case class Pattern1(p1: String, p2: String)

object Pattern1 {
  implicit val jsonReads: Reads[Pattern1] = (
    (JsPath \ "p1").read[String] and
      (JsPath \ "p2").read[String]
    ) (Pattern1.apply _)

  implicit val jsonWrites: Writes[Pattern1] = (p: Pattern1) =>
    Json.obj(
      "p1" -> p.p1,
      "p2" -> p.p2
    )
}
```

### レスポンスの確認

#### 正常系

**リクエスト**

```
POST http://localhost:9000/pattern1
Content-Type: application/json

{
  "p1": "this is p1",
  "p2": "this is p2"
}
```

**レスポンス**

```
{
  "result": {
    "p1": "this is p1",
    "p2": "this is p2"
  }
}
```

### 異常系

全部のパラメータ`p1`,`p2`に不正な値を設定してリクエストします。
**全部のパラメータをチェックしてくれていることがわかります**。

**リクエスト**

```
POST http://localhost:9000/pattern1
Content-Type: application/json

{
  "p1": 1,
  "p2": 2
}
```

**レスポンス**

```
{
  "result": "failure",
  "error": {
    "obj.p2": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p1": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ]
  }
}
```

## (Pattern2)パラメータが23個以上: for-yieldを使ってケースクラスにマッピングする

### 実装

```Pattern2.scala
package pattern

import play.api.libs.json._
import play.api.libs.json.Reads._

case class Pattern2(
                     p1: String,
                     p2: String,
                     p3: String,
                     p4: String,
                     p5: String,
                     p6: String,
                     p7: String,
                     p8: String,
                     p9: String,
                     p10: String,
                     p11: String,
                     p12: String,
                     p13: String,
                     p14: String,
                     p15: String,
                     p16: String,
                     p17: String,
                     p18: String,
                     p19: String,
                     p20: String,
                     p21: String,
                     p22: String,
                     p23: String,
                   )

object Pattern2 {

  implicit val jsonWrites: Writes[Pattern2] = (p: Pattern2) => {
    Json.obj(
      "p1" -> p.p1,
      "p2" -> p.p2,
      "p3" -> p.p3,
      "p4" -> p.p4,
      "p5" -> p.p5,
      "p6" -> p.p6,
      "p7" -> p.p7,
      "p8" -> p.p8,
      "p9" -> p.p9,
      "p10" -> p.p10,
      "p11" -> p.p11,
      "p12" -> p.p12,
      "p13" -> p.p13,
      "p14" -> p.p14,
      "p15" -> p.p15,
      "p16" -> p.p16,
      "p17" -> p.p17,
      "p18" -> p.p18,
      "p19" -> p.p19,
      "p20" -> p.p20,
      "p21" -> p.p21,
      "p22" -> p.p22,
      "p23" -> p.p23
    )
  }

  // JSON読み込み、パラメータチェックのためのimplicit peter
  implicit val jsonReads: Reads[Pattern2] = {

    for {
      p1 <- (JsPath \ "p1").read[String]
      p2 <- (JsPath \ "p2").read[String]
      p3 <- (JsPath \ "p3").read[String]
      p4 <- (JsPath \ "p4").read[String]
      p5 <- (JsPath \ "p5").read[String]
      p6 <- (JsPath \ "p6").read[String]
      p7 <- (JsPath \ "p7").read[String]
      p8 <- (JsPath \ "p8").read[String]
      p9 <- (JsPath \ "p9").read[String]
      p10 <- (JsPath \ "p10").read[String]
      p11 <- (JsPath \ "p11").read[String]
      p12 <- (JsPath \ "p12").read[String]
      p13 <- (JsPath \ "p13").read[String]
      p14 <- (JsPath \ "p14").read[String]
      p15 <- (JsPath \ "p15").read[String]
      p16 <- (JsPath \ "p16").read[String]
      p17 <- (JsPath \ "p17").read[String]
      p18 <- (JsPath \ "p18").read[String]
      p19 <- (JsPath \ "p19").read[String]
      p20 <- (JsPath \ "p20").read[String]
      p21 <- (JsPath \ "p21").read[String]
      p22 <- (JsPath \ "p22").read[String]
      p23 <- (JsPath \ "p23").read[String]

    } yield Pattern2(
      p1,
      p2,
      p3,
      p4,
      p5,
      p6,
      p7,
      p8,
      p9,
      p10,
      p11,
      p12,
      p13,
      p14,
      p15,
      p16,
      p17,
      p18,
      p19,
      p20,
      p21,
      p22,
      p23
    )
  }

}
```

### レスポンスの確認

#### 正常系

**リクエスト**

```
POST http://localhost:9000/pattern2
Content-Type: application/json

{
  "p1": "this is p1",
  "p2": "this is p2",
  "p3": "this is p3",
  "p4": "this is p4",
  "p5": "this is p5",
  "p6": "this is p6",
  "p7": "this is p7",
  "p8": "this is p8",
  "p9": "this is p9",
  "p10": "this is p10",
  "p11": "this is p11",
  "p12": "this is p12",
  "p13": "this is p13",
  "p14": "this is p14",
  "p15": "this is p15",
  "p16": "this is p16",
  "p17": "this is p17",
  "p18": "this is p18",
  "p19": "this is p19",
  "p20": "this is p20",
  "p21": "this is p21",
  "p22": "this is p22"
}
```

**レスポンス**

```
{
  "result": {
    "p1": "this is p1",
    "p2": "this is p2",
    "p3": "this is p3",
    "p4": "this is p4",
    "p5": "this is p5",
    "p6": "this is p6",
    "p7": "this is p7",
    "p8": "this is p8",
    "p9": "this is p9",
    "p10": "this is p10",
    "p11": "this is p11",
    "p12": "this is p12",
    "p13": "this is p13",
    "p14": "this is p14",
    "p15": "this is p15",
    "p16": "this is p16",
    "p17": "this is p17",
    "p18": "this is p18",
    "p19": "this is p19",
    "p20": "this is p20",
    "p21": "this is p21",
    "p22": "this is p22",
    "p23": "this is p23"
  }
}
```

#### 異常系

全部のパラメータ`p1`~`p23`に不正な値を設定してリクエストします。
**全部エラーのパラメータですが、レスポンスからは最初のパラメータ`p1`の情報しか確認できません**。

**リクエスト**

```
POST http://localhost:9000/pattern2
Content-Type: application/json

{
  "p1": 1,
  "p2": 2,
  "p3": 3,
  "p4": 4,
  "p5": 5,
  "p6": 6,
  "p7": 7,
  "p8": 8,
  "p9": 9,
  "p10": 10,
  "p11": 11,
  "p12": 12,
  "p13": 13,
  "p14": 14,
  "p15": 15,
  "p16": 16,
  "p17": 17,
  "p18": 18,
  "p19": 19,
  "p20": 20,
  "p21": 21,
  "p22": 22,
  "p23": 23
}
```

**レスポンス**

```
{
  "result": "failure",
  "error": {
    "obj.p1": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ]
  }
}
```


## (Pattern3)パラメータが23個以上: play-json-extensionを使ってケースクラスにマッピングする

play-json-extensionsを使っています。
https://github.com/xdotai/play-json-extensions

### 実装

```Patten3.scala
package pattern

import ai.x.play.json.Jsonx

case class Pattern3(
                     p1: String,
                     p2: String,
                     p3: String,
                     p4: String,
                     p5: String,
                     p6: String,
                     p7: String,
                     p8: String,
                     p9: String,
                     p10: String,
                     p11: String,
                     p12: String,
                     p13: String,
                     p14: String,
                     p15: String,
                     p16: String,
                     p17: String,
                     p18: String,
                     p19: String,
                     p20: String,
                     p21: String,
                     p22: String,
                     p23: String,
                   )

object Pattern3 {
  implicit val jsonExampleFormat1 = Jsonx.formatCaseClassUseDefaults[Pattern3]
}
```
### レスポンスの確認

#### 正常系

**リクエスト**

```
POST http://localhost:9000/pattern3
Content-Type: application/json

{
  "p1": "this is p1",
  "p2": "this is p2",
  "p3": "this is p3",
  "p4": "this is p4",
  "p5": "this is p5",
  "p6": "this is p6",
  "p7": "this is p7",
  "p8": "this is p8",
  "p9": "this is p9",
  "p10": "this is p10",
  "p11": "this is p11",
  "p12": "this is p12",
  "p13": "this is p13",
  "p14": "this is p14",
  "p15": "this is p15",
  "p16": "this is p16",
  "p17": "this is p17",
  "p18": "this is p18",
  "p19": "this is p19",
  "p20": "this is p20",
  "p21": "this is p21",
  "p22": "this is p22",
  "p23": "this is p23"
}
```

**レスポンス**

```
{
  "result": {
    "p1": "this is p1",
    "p2": "this is p2",
    "p3": "this is p3",
    "p4": "this is p4",
    "p5": "this is p5",
    "p6": "this is p6",
    "p7": "this is p7",
    "p8": "this is p8",
    "p9": "this is p9",
    "p10": "this is p10",
    "p11": "this is p11",
    "p12": "this is p12",
    "p13": "this is p13",
    "p14": "this is p14",
    "p15": "this is p15",
    "p16": "this is p16",
    "p17": "this is p17",
    "p18": "this is p18",
    "p19": "this is p19",
    "p20": "this is p20",
    "p21": "this is p21",
    "p22": "this is p22",
    "p23": "this is p23"
  }
}
```

#### 異常系

全部のパラメータ`p1`~`p23`に不正な値を設定してリクエストします。
**Pattern2と同様、全部エラーのパラメータになっています**。
**結果はPattern2と異なり、レスポンスからパラメータ`p1`～`p23`のチェック結果を確認できます**。

**リクエスト**

```
POST http://localhost:9000/pattern3
Content-Type: application/json

{
  "p1": 1,
  "p2": 2,
  "p3": 3,
  "p4": 4,
  "p5": 5,
  "p6": 6,
  "p7": 7,
  "p8": 8,
  "p9": 9,
  "p10": 10,
  "p11": 11,
  "p12": 12,
  "p13": 13,
  "p14": 14,
  "p15": 15,
  "p16": 16,
  "p17": 17,
  "p18": 18,
  "p19": 19,
  "p20": 20,
  "p21": 21,
  "p22": 22,
  "p23": 23
}
```

**レスポンス**

```
{
  "result": "failure",
  "error": {
    "obj.p1": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p2": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p3": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p4": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p5": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p6": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p7": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p8": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p9": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p10": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p11": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p12": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p13": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p14": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p15": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p16": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p17": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p18": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p19": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p20": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p21": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p22": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ],
    "obj.p23": [
      {
        "msg": [
          "error.expected.jsstring"
        ],
        "args": []
      }
    ]
  }
}
```

いまのところ、Pattern3でケースクラスにマッピングした後にバリデーションチェックを自前実装するのが良さそう。という自己結論。

何かいい実装方法あったら教えて頂けると助かります。。。