本記事ではSpring Bootで@RequestBodyアノテーションを使用し、
JSONがオブジェクトにマッピングされる際の注意点をお伝えします。
※ Kotlinでの挙動となります。
結論から述べると、non-nullable型にマッピングする際はデフォルト値に気をつけたほうがいいです!
Nullableなプロパティにマッピングする場合
以下がサンプルのコードです。
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
@RestController
class NullableController {
@PostMapping("/nullable")
fun nullable(@RequestBody nullableType: NullableType) = nullableType
}
data class NullableType(
val int: Int?,
val double: Double?,
val float: Float?,
val long: Long?,
val short: Short?,
val byte: Byte?,
val char: Char?,
val boolean: Boolean?,
val string: String?
)
上記コードでは@RequestBodyアノテーションをもちいて、リクエストのJSONをオブジェクトへマッピングしています。
マッピングされるNullableTypeはデータクラスで、すべてのプロパティがnullableで定義されています。
こちらのAPIにリクエストを投げると以下のように返ってきます。
$ curl -X POST localhost:8080/nullable -H "Content-Type:application/json" -d '{"int": 1, "double": 1.1, "float": 1.2, "long": 2, "short": 3, "byte": 4, "char": "5", "boolean": true, "string": "test"}'
{"int":1,"double":1.1,"float":1.2,"long":2,"short":3,"byte":4,"char":"5","boolean":true,"string":"test"}
では、今度はカラのJSONを投げたとき、NullableTypeはどのようにマッピングされるでしょうか?
正解はこちら↓
$ curl -X POST localhost:8080/nullable -H "Content-Type:application/json" -d '{}'
{"int":null,"double":null,"float":null,"long":null,"short":null,"byte":null,"char":null,"boolean":null,"string":null}
NullableTypeプロパティはすべてnullableであるため、リクエストのJSONに定義されていないパラメータはnullとして定義されました。
Non-nullなプロパティにマッピングする場合
以下がサンプルのコードです。
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
@RestController
class NonNullController {
@PostMapping("/non-null")
fun nonNull(@RequestBody nonNullType: NonNullType) = nonNullType
}
data class NonNullType(
val int: Int,
val double: Double,
val float: Float,
val long: Long,
val short: Short,
val byte: Byte,
val char: Char,
val boolean: Boolean,
val string: String
)
こちらはマッピングされるNonNullTypeのプロパティがすべてnon-nullで定義されています。
こちらのAPIもリクエストを投げると以下のように返ってきます。
$ curl -X POST localhost:8080/non-null -H "Content-Type:application/json" -d '{"int": 1, "double": 1.1, "float": 1.2, "long": 2, "short": 3, "byte": 4, "char": "5", "boolean": true, "string": "test"}'
{"int":1,"double":1.1,"float":1.2,"long":2,"short":3,"byte":4,"char":"5","boolean":true,"string":"test"}
つぎにNullableのときと同様にカラのJSONを投げるのですが、プロパティの型によってマッピングの挙動が異なります。
JSON内でString型のプロパティが定義されていない場合
前述のリクエストからString型のパラメータだけ削除してPOSTしてみるとどうなるでしょうか?
$ curl -X POST localhost:8080/non-null -H "Content-Type:application/json" -d '{"int": 1, "double": 1.1, "float": 1.2, "long": 2, "short": 3, "byte": 4, "char": "5", "boolean": true}'
{"timestamp":"2018-09-10T00:00:00.000+0000","status":400,"error":"Bad Request","message":"JSON parse error: Instantiation of [simple type, class com.example.demo.NonNullType] value failed for JSON property string due to missing (therefore NULL) value for creator parameter string which is a non-nullable type; nested exception is com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class com.example.demo.NonNullType] value failed for JSON property string due to missing (therefore NULL) value for creator parameter string which is a non-nullable type\n at [Source: (PushbackInputStream); line: 1, column: 103] (through reference chain: com.example.demo.NonNullType[\"string\"])","path":"/non-null"}
結果としてBad Requestが返ってきました。
NonNullTypeでstringプロパティが定義されているため、JSON parse errorとして怒られるのは当然のように思えます。
JSON内でプリミティブ型のプロパティが定義されていない場合
こんどは前述のリクエストのString型のパラメータ以外を削除してPOSTしてみます。
$ curl -X POST localhost:8080/non-null -H "Content-Type:application/json" -d '{"string": "test"}
{"int":0,"double":0.0,"float":0.0,"long":0,"short":0,"byte":0,"char":"\u0000","boolean":false,"string":"test"}
結果はJSONにパラメータを渡していなかったにもかかわらず、デフォルト値を付与したうえでNonNullTypeにマッピングされました。
jackson-kotlin-moduleにおいて、Javaのプリミティブ型にあたるプロパティはデフォルト値が付与された状態でマッピングされるようです。
もし参照型と同様に、JSON内でパラメータが存在しないときBad Requestを返すようにするには、以下の設定が必要です。
spring.jackson.deserialization.fail-on-null-for-primitives=true