[Spring] 스프링 Custom Enum Deserializer 구현으로 JSON Enum null 로 파싱하기
https://gengminy.tistory.com/48
앞서 스프링에서 Custom Enum Constraint Validator 를 Reflection 으로까지 구현했다.
하지만 딱 한 가지가 남았는데....
보기 싫었던 다음과 같은 반복적인 코드가 있다.
// Enum Validation 을 위한 코드, enum 에 속하지 않으면 null 리턴
@JsonCreator
public static EventStatus fromEventStatus(String val) {
return Arrays.stream(values())
.filter(type -> type.getName().equals(val))
.findAny()
.orElse(null);
}
Request 에서 제공한 Enum 필드에 대해 역직렬화 해주는 @JsonCreator
@JsonCreator 를 구현하지 않는다면
null 값을 대입하는 대신 JSON Parse Error 를 발생시킨다.
그래서 Enum Validation 을 위해선
Enum 에 속하지 않는 Constant 에 대해서 null 을 반환하도록 할 필요가 있다.
하지만 Enum 마다 반복적으로 구현해야 해서 이를 줄이고 싶었다.
여러 가지 방법을 시도하고 실패를 반복하며....
결국 구글링으로 방법을 찾았다!
📌 원하고자 하는 목표
@Getter
@AllArgsConstructor
@EnumClass
public enum EventStatus {
PREPARING("PREPARING", "준비중"),
OPEN("OPEN", "진행중"),
CALCULATING("CALCULATING", "정산중"),
CLOSED("CLOSED", "지난공연"),
DELETED("DELETED", "삭제된공연");
private final String name;
@JsonValue private final String value;
}
위 처럼 @EnumClass 라는 커스텀 어노테이션 하나로
@JsonCreator 에서 실행하는 로직을 축약하고 싶었다.
📝 EnumClass
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonDeserialize(using = CustomEnumDeserializer.class)
public @interface EnumClass {}
커스텀 어노테이션 @EnumClass 를 정의한다.
@JsonDeserialize 라는 어노테이션을 통해 사용하고 싶은 Deserializer 를 지정할 수 있다.
이런 식으로 Jackson 관련 어노테이션을 상속시킬 때는
반드시 @JacksonAnnotationsInside 를 붙여야한다.
그렇지 않으면 이 어노테이션 자체에 Jackson 관련 코드가 적용되어서
원하지 않는 방향으로 실행이 된다.
📝 CustomEnumDeserializer
public class CustomEnumDeserializer extends StdDeserializer<Enum<?>>
implements ContextualDeserializer {
public CustomEnumDeserializer() {
this(null);
}
protected CustomEnumDeserializer(Class<?> vc) {
super(vc);
}
@SuppressWarnings("unchecked")
@Override
public Enum<?> deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
JsonNode jsonNode = jp.getCodec().readTree(jp);
JsonNode nameNode = jsonNode.get("name");
if (nameNode == null) return null;
String text = jsonNode.asText();
Class<? extends Enum> enumType = (Class<? extends Enum>) this._valueClass;
return Arrays.stream(enumType.getEnumConstants())
.filter(constant -> constant.name().equals(text))
.findAny()
.orElse(null);
}
@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
throws JsonMappingException {
return new CustomEnumDeserializer(property.getType().getRawClass());
}
}
모든 Enum 인스턴스에 대해 적용하기 위해 제네릭으로 Enum<?> 을 적용한다.
또 주목할만한 점은 ContextualDeserializer 를 implementation 하는 점인데
현재 Context 의 Target 클래스 타입을 통해 Deserializer 를 재정의할 필요가 있기 때문이다.
그렇지 않다면 현재 타입을 알 수 없어 this._valueClass 호출 시 NPE 가 발생한다.
Enum 타입이 고정되어 있다면 직접 Status.class 같이 생성자에 넣어주면 되지만,
현재 모든 추상 클래스 타입에 대해 적용해야하기 때문에 이 과정이 반드시 필요하다.
deserialize 구현부에서는 jsonNode 의 "name" 을 가져와
Enum Constant 의 name 으로 비교를 수행한다.
nameNode 로 굳이 한 번 더 나누어준 이유는
asText() 메소드가 null 에 대해 수행할 수 있는 가능성이 있기 때문
없는 Enum Constant 에 대해 정상적으로 null 을 반환하고 Validation 해준다.
이로써 Enum 관련 코드가 아주 깔끔해졌다!
🔍 레퍼런스
https://d2.naver.com/helloworld/0473330
위의 글이 많은 도움이 되었다.
🌎 구현 과정 관련 게시글
📎 Custom Enum Validator 구현하기
https://gengminy.tistory.com/47
📎 Reflection 을 이용하여 Enum Validator 개선하기
https://gengminy.tistory.com/48
📎 Custom Enum Deserializer 구현하여 Enum 에 없는 값 null 로 파싱하기
https://gengminy.tistory.com/49