Kotlin springでJsonからEnumへ変換する

Jsonからenumの変換方法いろいろ
## バージョン
org.springframework.boot 2.6.2
## 参考クラス
Controllerクラス
```Controller.kt
@RequestMapping("/test")
@ResponseBody
fun test(@RequestBody json: JsonObject): String {
    println(json.toString())
    return "OK"
}
```
リクエストのJsonクラス
```Request.kt
public data class JsonObject(
    val stringValue: String,
    val enumValue: JsonEnum
)
```
## アノテーションなし
変換対象のenum
```enum.kt
public enum class JsonEnum(
    val value: String
) {
    ENUM_TYPE_1("type_1"),
    ENUM_TYPE_2("type_2"),
    ENUM_TYPE_3("type_3");
}
```
### enum名を表す文字列で送信
```Request.json
{
  "stringValue": "test",
  "enumValue": "ENUM_TYPE_2"
}
```
リクエストの出力（Controller内で標準出力）
enumは正常に変換される。
```
JsonObject(stringValue=test, enumValue=ENUM_TYPE_2)
```
### 不正なenum名で送信
```Request.json
{
  "stringValue": "test",
  "enumValue": "ENUM_TYPE_0"
}
```
例外発生
```
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `sample.JsonEnum` from String "ENUM_TYPE_0": not one of the values accepted for Enum class: [ENUM_TYPE_1, ENUM_TYPE_2, ENUM_TYPE_3]; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `sample.JsonEnum` from String "ENUM_TYPE_0": not one of the values accepted for Enum class: [ENUM_TYPE_1, ENUM_TYPE_2, ENUM_TYPE_3]<EOL> at [Source: (PushbackInputStream); line: 1, column: 35] (through reference chain: sample.JsonObject["enumValue"])]
```
### enumの0から始まる列挙順で送信
```Request.json
{
  "stringValue": "test",
  "enumValue": "2"
}
```
リクエストの出力（Controller内で標準出力）
enumは正常に変換される。
```
JsonObject(stringValue=test, enumValue=ENUM_TYPE_3)
```
### 不正な列挙順で送信
```Request.json
{
  "stringValue": "test",
  "enumValue": "3"
}
```
例外発生
```
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `sample.JsonEnum` from String "3": not one of the values accepted for Enum class: [ENUM_TYPE_1, ENUM_TYPE_2, ENUM_TYPE_3]; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `sample.JsonEnum` from String "3": not one of the values accepted for Enum class: [ENUM_TYPE_1, ENUM_TYPE_2, ENUM_TYPE_3]<EOL> at [Source: (PushbackInputStream); line: 1, column: 36] (through reference chain: sample.JsonObject["enumValue"])]
```
### enumのプロパティ名を文字列で送信
```Request.json
{
  "stringValue": "test",
  "enumValue": "type_2"
}
```
例外発生
```
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `sample.JsonEnum` from String "type_3": not one of the values accepted for Enum class: [ENUM_TYPE_1, ENUM_TYPE_2, ENUM_TYPE_3]; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `sample.JsonEnum` from String "type_3": not one of the values accepted for Enum class: [ENUM_TYPE_1, ENUM_TYPE_2, ENUM_TYPE_3]<EOL> at [Source: (PushbackInputStream); line: 1, column: 35] (through reference chain: sample.JsonObject["enumValue"])]
```
## アノテーションあり
enumにJsonから変換する際に使用するプロパティに＠JsonValueのアノテーション付与
```enum.kt
public enum class JsonEnum(
    @JsonValue
    val value: String
) {
    ENUM_TYPE_1("type_1"),
    ENUM_TYPE_2("type_2"),
    ENUM_TYPE_3("type_3");
}
```
### enumを表す任意の文字列で送信
```Request.json
{
  "stringValue": "test",
  "enumValue": "type_2"
}
```
リクエストの出力（Controller内で標準出力）
enumは正常に変換される。
```
JsonObject(stringValue=test, enumValue=ENUM_TYPE_2)
```
### 不正なenumを表す文字列で送信
```Request.json
{
  "stringValue": "test",
  "enumValue": "type_4"
}
```
例外発生
```
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `sample.JsonEnum` from String "type_4": not one of the values accepted for Enum class: [type_1, type_2, type_3]; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `sample.JsonEnum` from String "type_4": not one of the values accepted for Enum class: [type_1, type_2, type_3]<EOL> at [Source: (PushbackInputStream); line: 1, column: 35] (through reference chain: sample.JsonObject["enumValue"])]
```
### enumの0から始まる列挙順で送信
```Request.json
{
  "stringValue": "test",
  "enumValue": "1"
}
```
リクエストの出力（Controller内で標準出力）
enumは正常に変換される。
```
JsonObject(stringValue=test, enumValue=ENUM_TYPE_2)
```
### 不正な列挙順で送信
```Request.json
{
  "stringValue": "test",
  "enumValue": "3"
}
```
例外発生
```
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `sample.JsonEnum` from String "3": not one of the values accepted for Enum class: [type_1, type_2, type_3]; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `sample.JsonEnum` from String "3": not one of the values accepted for Enum class: [type_1, type_2, type_3]<EOL> at [Source: (PushbackInputStream); line: 1, column: 35] (through reference chain: sample.JsonObject["enumValue"])]
```
### enum名を表す文字列で送信
```Request.json
{
  "stringValue": "test",
  "enumValue": "ENUM_TYPE_1"
}
```
例外発生
```
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `sample.JsonEnum` from String "ENUM_TYPE_1": not one of the values accepted for Enum class: [type_1, type_2, type_3]; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `sample.JsonEnum` from String "ENUM_TYPE_1": not one of the values accepted for Enum class: [type_1, type_2, type_3]<EOL> at [Source: (PushbackInputStream); line: 1, column: 35] (through reference chain: sample.JsonObject["enumValue"])]
```
列挙順の数値と＠JsonValueに指定した値から変換できてしまうので、少しややこしいです。
＠JsonValueに指定した値が数値だとよりわかりにくくなりそうな気が。。
後述の＠JsonCreatorを追加したほうが無難です。

## アノテーションあり（enum生成処理あり）
enum生成処理に＠JsonCreatorを付与
```enum.kt
public enum class JsonEnum(
    @JsonValue
    val value: String
) {
    ENUM_TYPE_1("type_1"),
    ENUM_TYPE_2("type_2"),
    ENUM_TYPE_3("type_3");

    companion object {
        @JvmStatic
        @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
        fun fromJson(value: String): JsonEnum {
            return values().find { it.value == value }
                ?: throw IllegalArgumentException("Bad Request $value")
        }
    }
}
```
Kotlinの場合、＠JsonCreatorを使用するcompanion object（≒static）関数には、＠JvmStaticをつけてあげないとライブラリから関数を見つけることができない。
＠JsonCreator関してはmodeを指定しないとエラーが発生する。（後述）
### enumを表す任意の文字列で送信
```Request.json
{
  "stringValue": "test",
  "enumValue": "type_1"
}
```
リクエストの出力（Controller内で標準出力）
enumは正常に変換される。
```
JsonObject(stringValue=test, enumValue=ENUM_TYPE_1)
```
### 不正なenumを表す文字列で送信
```Request.json
{
  "stringValue": "test",
  "enumValue": "type_0"
}
```
例外発生
作成したenum生成処理（companion object）内での発生
```
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of `sample.JsonEnum`, problem: Bad Request type_0; nested exception is com.fasterxml.jackson.databind.exc.ValueInstantiationException: Cannot construct instance of `sample.JsonEnum`, problem: Bad Request type_0<EOL> at [Source: (PushbackInputStream); line: 1, column: 35] (through reference chain: sample.JsonObject["enumValue"])]
```
### enumの0から始まる列挙順で送信
```Request.json
{
  "stringValue": "test",
  "enumValue": "2"
}
```
例外発生
作成したenum生成処理（companion object）内での発生
```
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of `sample.JsonEnum`, problem: Bad Request 2; nested exception is com.fasterxml.jackson.databind.exc.ValueInstantiationException: Cannot construct instance of `sample.JsonEnum`, problem: Bad Request 2<EOL> at [Source: (PushbackInputStream); line: 1, column: 35] (through reference chain: sample.JsonObject["enumValue"])]
```
### enum名を表す文字列で送信
```Request.json
{
  "stringValue": "test",
  "enumValue": "ENUM_TYPE_1"
}
```
例外発生
作成したenum生成処理（companion object）内での発生
```
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of `sample.JsonEnum`, problem: Bad Request ENUM_TYPE_1; nested exception is com.fasterxml.jackson.databind.exc.ValueInstantiationException: Cannot construct instance of `sample.JsonEnum`, problem: Bad Request ENUM_TYPE_1<EOL> at [Source: (PushbackInputStream); line: 1, column: 35] (through reference chain: sample.JsonObject["enumValue"])]
```
## ＠JsonCreatorのmode指定に関して
以下で報告されているようにmode指定しないとエラーが発生する。
https://github.com/FasterXML/jackson-module-kotlin/issues/336
mode指定なしのenum
```enum.kt
public enum class JsonEnum(
    @JsonValue
    val value: String
) {
    ENUM_TYPE_1("type_1"),
    ENUM_TYPE_2("type_2"),
    ENUM_TYPE_3("type_3");

    companion object {
        @JvmStatic
        @JsonCreator
        fun fromJson(value: String): JsonEnum {
            return values().find { it.value == value }
                ?: throw IllegalArgumentException("Bad Request $value")
        }
    }
}
```
正常に動いたものと同じ値で送信
```Request.json
{
  "stringValue": "test",
  "enumValue": "type_1"
}
```
例外発生
```
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Input mismatch reading Enum `sample.JsonEnum`: properties-based `@JsonCreator` ([method sample.JsonEnum#fromJson(java.lang.String)]) expects JSON Object (JsonToken.START_OBJECT), got JsonToken.VALUE_STRING; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Input mismatch reading Enum `sample.JsonEnum`: properties-based `@JsonCreator` ([method sample.JsonEnum#fromJson(java.lang.String)]) expects JSON Object (JsonToken.START_OBJECT), got JsonToken.VALUE_STRING<EOL> at [Source: (PushbackInputStream); line: 1, column: 35] (through reference chain: sample.JsonObject["enumValue"])]
```
## まとめ
| アノテーション | enum名 | 列挙順 | 指定の値 |
| :------------: | :-----:| :----: | :------: |
| なし   | 〇 | 〇 | ✕ |
| ＠JsonValue | ✕ | 〇 | 〇 |
| ＠JsonValue＋＠JsonCreator| ✕ | ✕ | 〇 |