JPA

JSON parse error: Cannot deserialize value of type `enum package`

MIN우 2023. 8. 6. 12:34
728x90

이게 무슨오류일까 ??

 

enum을 validationCheck해주는 어노테이션을 만들다가 오류가 발생했다.

 

 

기본적으로 json body를 받을때, json to object 과정에서 deserialize 는Jackson라이브러리에서 실행됩니다.

이 경우 enum value의 name과 완전 동일한경우, 기본 deserialize가 있기에 문제는 없으나, 변수가 조금이라도 틀리게되면 바로 이런 에러를 맞이하게됩니다.

 

 JSON parse error: Cannot deserialize value of type `enum package` 

 

이경우 해당 에러를 맞이하지않고, 내가 지정한 setter를 사용하게끔 지원하는 @JsonCreator라는 어노테이션이 필요합니다.


youth,adult,oldMan;

@JsonCreator
public static VideoAgeCategoryEnum fromVideoAgeCategoryEnum(String val){

return Arrays.stream(VideoAgeCategoryEnum.values())
.filter(testEnum -> testEnum.name().equals(val))
.findFirst()
.orElse(null);
}

다음과 같이 지정하는경우, API 요청을 받아올때, VideoAgeCategoryEnum 값으로 받은 string은 @JsonCreator를 지정한 메서드의 파라미터로 들어가게 되고, 해당 메서드 기능을 통해, request dto에 저장되게됩니다.

 

쉽게 이야기하면 json body 받을 때 json to object과정에서 deserialize 는 jackson라이브러리에서 실행시켜준다.

하지만 enum value는 name과 동일했을 때는 deserialize 에서 문제가없지만 변수가 틀리게되면 deserialize가 실패해서 다음과 같은

에러가 뜨게된다. 이럴경우에 enum으로 들어오는 값을 @JsonCreator를 사용하여 틀린값이 들어오든 맞는 값이 들어오든

deserialize가 되게한다!

 

 

추가적으로 Enum값을 Validation하는 방법을 적어보겠습니다.

 

1. 어노테이션을 직접만들고, 구현체를 만들어줍니다.

@Constraint(validatedBy = EnumValidator.class)
/* 해당 annotation이 실행 할 ConstraintValidator 구현체를 `EnumValidator`로 지정합니다. */
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
/* 해당 annotation은 메소드, 필드, 파라미터에 적용 할 수 있습니다. */
@Retention(RetentionPolicy.RUNTIME)
/* annotation을 Runtime까지 유지합니다. */
public @interface EnumValid {
    String message() default "Invalid value. This is not permitted.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    Class<? extends java.lang.Enum<?>> enumClass();
}

2. 해당 어노테이션지정 시 실제 실행될 로직을 작성해줍니다.

package com.example.springboot.handler.Exception;

import com.example.springboot.domain.video.entity.VideoAgeCategoryEnum;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class EnumValidator implements ConstraintValidator<EnumValid, Enum> {
    private EnumValid annotation;

    @Override
    public void initialize(EnumValid constraintAnnotation) {
        this.annotation = constraintAnnotation;
    }




    //isValid() : 실제 유효성 판단을 내리는 곳으로
    // initialize를 통해서 Enum 타입들을 얻었으니 contains 메소드를 활용해서 현재 Param 클래스의
    // gender 변수로 들어온 값이 types에 존재하는지만 판단해주면 됩니다.

    @Override
    public boolean isValid(Enum value, ConstraintValidatorContext context) {
        boolean result = false;
        Object[] enumValues = this.annotation.enumClass().getEnumConstants();
        if (enumValues != null) {
            for (Object enumValue : enumValues) {
                if (value == enumValue) {
                    result = true;
                    break;
                }
            }
        }
        return result;
    }
}

3. 해당하는 dto에 어노테이션을 붙여주고 Controller에서 @Valid , BindingResult 선언을 통해 오류를 반환할 수 있도록합니다.

    @EnumValid(enumClass = VideoAgeCategoryEnum.class, message = "정해진 값들만 넣을 수 있습니다")
    private VideoAgeCategoryEnum ageCategoey;

4. CustomApiException을 만들어주고

package com.example.springboot.handler.Exception.ex;

import java.util.Map;

public class CustomVaildationApiException extends RuntimeException {

    //객체를 구분할 떄!! serialvseionUID
    private Map<String,String> errorMap;


    public CustomVaildationApiException(String message) {
        super(message);
    }

    public CustomVaildationApiException(String message, Map<String, String> errorMap) {
        super(message);
        this.errorMap = errorMap;
    }
    public Map<String,String> getErrorMap(){
        return errorMap;
    }
}

5. 해당 오류를 반환해주도록 합시다.

package com.example.springboot.handler.Exception;

import com.example.springboot.common.dto.CMResDto;
import com.example.springboot.handler.Exception.ex.CustomApiException;
import com.example.springboot.handler.Exception.ex.CustomVaildationApiException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;

@RestController
@ControllerAdvice // 모든 exception을 낚아챔
public class ControllerExceptionHandler {


    @ExceptionHandler(CustomVaildationApiException.class) //RuntimeException 함수를 가로챔
    public ResponseEntity<?> validationApiException(CustomVaildationApiException e){

        //CMResDto,Script비교
        // 클라이언트에게 응답할때는 script가 좋음

        return new ResponseEntity<>(new CMResDto<>(-400,e.getMessage(),e.getErrorMap()),HttpStatus.BAD_REQUEST);
    }


}
{
    "code": -400,
    "message": "유효성검사 실패함",
    "data": {
        "ageCategoey": "정해진 값들만 넣을 수 있습니다"
    }
}

이제 Enum값도 하나의 어노테이션을 통해 validationCheck를 할 수 있게 되었습니다.

 

 

에러페이지 작동 원리

  • 컨트롤러에서 예외가 발생할 경우 컨트롤러 → 인터셉터 → 서블릿 → 필터 → WAS 순으로 예외가 전달되면 WAS는 에러 페이지 정보를 확인하고 다시 에러 페이지 출력을 위해 재요청
  • HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 - 예외 발생 -> 인터셉터 -> 서블릿 -> 필터 -> WAS - 예외 페이지 정보 확인 -> 필터 -> 서블릿 -> 인터셉터 -> 예외 페이지 컨트롤러 -> 예외 페이지 (View)

Auth Level: doFilter

인증 인가에 관련된 인증처리는 doFilter단에서 처리를 주로한다.

Controller Level: @ExceptionHandler

Controller 메서드 내의 하위 서비스 (Service, Repository등등)에서 예외가 발생하더라도,

중간에 처리하지 않는 이상 Controller단까지 예외가 던져지게 되고 @ExceptionHandler가 예외를 처리하게 된다.

Checked Exception, Runtime Exception 상관 없이 Controller까지 예외를 throw하면 처리가 가능하다.

Global Level: @ControllerAdvice

하나의 Controller가 아닌 여러 Controller에서 발생하는 예외를 처리하려면 @ControllerAdvice를 사용해야 한다.

@ControllerAdvice는 모든 Controller에서 발생하는 예외를 처리할 수 있게 해주는 애노테이션이다.

DispatcherServlet에서 발생하는 예외를 전역적으로 처리해준다.

DispatcherServlet에서 발생하는 예외만 처리할 수 있고 Filter에서 발생하는 예외는 따로 처리를 하지 않으면 처리가 불가능하다.

Controller의 @ExceptionHandler와 ControllerAdvice의 @ExceptionHandler중 높은 우선순위는?

Controller의 @ExceptionHandler가 먼저다.

728x90