안녕하세요!
키크니 개발자 입니다. 🦒
문제상황
RequestBody를 사용하면서 dto 로 data를 받기 위해 Enum을 사용하였습니다.
상수값으로 data를 전달하면 바로 받을 수 있지만,
이번 상황은 상수값에 대한 속성을 JSON에 포함시켜 data를 받아와야하는 상황이었습니다.
Fruit enum class
@Getter
public enum Fruit {
APPLE("사과"),
BANANA("바나나"),
GRAPE("포도"),
ORANGE("오렌지");
private final String name;
Fruit(String name) {
this.name = name;
}
}
FruitController
@PostMapping("/fruit")
public String saveFruit(@RequestBody FruitDto body) {
log.info("save fruit. body={}", body.toString());
fruitService.saveFruit(body);
return "OK";
}
FruitDto
@Data
public class FruitDto {
private Fruit fruit;
private Integer count;
}
request data
상수값의 속성으로 data를 넣고 request를 하면 json parse error인 InvalidFormatException exception이 발생하였습니다.
(request data를 "fruit" : "APPLE" 로 넣어주면 요청이 잘 됩니다.)
{
"fruit" : "사과",
"count" : 1
}
자세한 error 내용
org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error:
Cannot deserialize value of type `xx.xxx.xxx.XXX` from String "사과": not one of the values accepted for Enum class:
[APPLE, BANANA, GRAPE, ORANGE];
nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException:
Cannot deserialize value of type `xx.xxx.xxx.XXX` from String "사과":
not one of the values accepted for Enum class: [APPLE, BANANA, GRAPE, ORANGE]
해결방법은?
처음에는 converter를 사용하면 되는줄 알았지만 @RequestParam을 사용하는 것이 아닌,
@RequestBody를 사용하면 무용지물이 된다는 것을 알았습니다.
검색을 하여 찾은 결과 Jackson 2.6부터는 @JsonProperty로 enum의 각 요소를 사용하여 직렬화/역직렬화 값을 지정할 수 있는 것을 알게되었습니다.
Jackson 2.6 이상 해결방법
- @JsonProperty를 사용합니다.
@Getter
public enum Fruit {
@JsonPropertyp("사과")
APPLE("사과"),
@JsonPropertyp("바나나")
BANANA("바나나"),
@JsonPropertyp("포도")
GRAPE("포도"),
@JsonPropertyp("오렌지")
ORANGE("오렌지");
private final String name;
Fruit(String name) {
this.name = name;
}
}
Jackson 2.6 이하 해결방법
- @JsonCreator를 사용합니다.
- @JsonCreator란?
- 해당 클래스 JSON 문자열을 받아서 객체를 생성할 때 변환기를 직접 만들고자 할 때 구현합니다.
- Jackson이 json을 deserialize해서 객체로 만들기 위해서는 객체를 생성하고 필드를 채울 수 있어야 합니다.
- 객체를 생성하는 것은 기본 생성자를 사용하고, 필드를 채우는 것은 필드가 pulbic 이면 직접할당하고, Private 이면 setter를 사용합니다. → 하지만 객체를 캡슐화하는 것은 기본 권장 사항이기 때문에 무조건 setter를 사용한다고 보면 됩니다.
- 즉, 생성자 + setter = @JsonCreator 라고 볼 수 있습니다.
- 해당 어노테이션은 생성자나 팩토리 메소드 위에 붙이면 jackson이 해당 함수를 통해 객체를 생성하고 필드를 생성과 동시에 채웁니다. (setter 함수가 필요없어지게 됩니다.)
- 해당 클래스 JSON 문자열을 받아서 객체를 생성할 때 변환기를 직접 만들고자 할 때 구현합니다.
@Getter
public enum Fruit {
APPLE("사과"),
BANANA("바나나"),
GRAPE("포도"),
ORANGE("오렌지");
private final String name;
Fruit(String name) {
this.name = name;
}
private static Map<String, Status> FORMAT_MAP = Stream // (1)
.of(Fruit.values())
.collect(Collectors.toMap(s -> s.name, Function.identity()));
@JsonCreator // (2)
public static Fruit fromString(String fruitKeyword) {
return Optional
.ofNullable(FORMAT_MAP.get(fruitKeyword))
.orElseThrow(() -> new IllegalArgumentException(fruitKeyword));
}
}
(1) enum의 상수값의 속성과 request 값과 일치하는 것이 있을 때 Map을 만듭니다.
private static Map<String, Status> FORMAT_MAP = Stream
.of(Status.values())
.collect(Collectors.toMap(s -> s.formatted, Function.identity()));
- Stream.of()로 Stream 을 생성합니다.
- values()는 enum의 요소(속성)들을 순서대로 Enum 타입의 배열로 리턴합니다.
- 값이 실제 요소여야 하는 일반적인 경우에 사용합니다. (Enum의 상수값을 Return 합니다.)
(2) FORMAT_MAP에서의 Key인 enum의 상수에 대한 속성값을 기준으로 value인 enum의 상수값을 return 합니다.
@JsonCreator // (2)
public static Status fromString(String string) {
return Optional
.ofNullable(FORMAT_MAP.get(string))
.orElseThrow(() -> new IllegalArgumentException(string));
}
- Optional.ofnullable() : nul인지 아닌지 확실할 수 없는 객체를 담고 있는 Optional 객체를 생성하며, null 이 넘어올 경우 NPE를 던지지 않고 Optional.empty()와 동일하게 비어있는 Optional 객체를 얻어옵니다. (해당 객체가 Null인지 아닌지 확신할 수 없는 상황에서 사용합니다.)
- Map.get(key) : Map 안에 있는 key에 대한 value을 가져옵니다.
참고로 Spring Boot에서 Jackson 버전을 확인하기 위해서는 아래를 참고해주세요.
💡 spring project 안에 External Libraries > `com.fasterxml.jackson.core`:jackson-core 버전을 확인하면 됩니다.
저는 spring boot 2.6.2를 사용하고 있어서 Jackson 버전이 2.13.1인 것으로 확인이 되었습니다.
(spring boot에서는 기본적으로 Jackson이 내장되어있습니다.)
References
https://stackoverflow.com/questions/31689107/deserializing-an-enum-with-jackson
https://kwonnam.pe.kr/wiki/java/jackson
'삽질일기' 카테고리의 다른 글
[GitHub] Github repository의 폴더에 흰색 화살표가 생기며 클릭이 안 되는 현상 (0) | 2022.12.06 |
---|---|
외부에서 local로 접속하기 (포트포워드) (0) | 2022.10.25 |
The AWS Access Key Id you provided does not exist in our records 해결 (0) | 2022.09.10 |
[Elasticsearch] auto_generate_synonyms_phrase_query = true error (0) | 2022.07.31 |
[Spring] springSecurityFilterChain 관련 에러 (Java 상위 버전으로 변경시 생기는 문제) (0) | 2022.07.24 |