🚀 프로젝트/🥁 두둥

[Spring] 스프링 Custom Enum Deserializer 구현으로 JSON Enum null 로 파싱하기

gengminy 2023. 2. 21. 16:57

https://gengminy.tistory.com/48

 

[Spring] 스프링 Enum Validator Reflection 으로 개선 및 구현하기

https://gengminy.tistory.com/47 [Spring] 스프링에서 Enum 클래스 Validation 추가하기 (Enum JSON parse error 해결) 스프링에서 일반적으로 RequestBody 의 값을 Validation 하는 방법 스프링 MVC 에서 @Valid 어노테이션을

gengminy.tistory.com

 

앞서 스프링에서 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<?> 을 적용한다.

 

또 주목할만한 점은 ContextualDeserializerimplementation 하는 점인데

현재 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