Post

(Java) Enum to Json / Enum to Object

  • EnumConstant 하나를 {fieldName:fieldValue} 형식으로 매핑하고 싶은 경우가 있다 ( 주로 뷰로 전달해야 할 때. )
  • 기본적으로 jackson의 ObjectMapper는 serialize/deserialize 할 때 Enum 코드만 내려주도록 되어 있다.
    • 즉, PaymentCode.CARD를 변환하면 "CARD"가 된다.
    • {code=1, koName="카드", enName="card"} 형태로 내려주려면, 뭔가 해줘야 한다

방법 1 ) Jackson / ObjectMapper를 사용하고 싶은 경우

방법 1-1 ) @JsonFormat.Shape.OBJECT

  • key:value 쌍을 내려주고 싶은 경우를 대비해 jackson은 JsonFormat.Shape.OBJECT 를 제공하고 있다. docs
  • 아래와 같이 지정해주면 알아서 serialize 시 key=value 형식으로 매핑해준다.
  • deserialize 시에는 그냥 CODE만 받아도 enum으로 받아준다. (controller에서 받을 때 등등)
1
2
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum EnumCls
  • 반면, 붙어 있는 애너테이션을 무시하고 싶다면?
    1
    
    objectMapper.disable(MapperFeature.USE_ANNOTATIONS);
    

방법 1-2 ) key:value 매핑해주는 Serializer를 등록해서 사용하고 싶은 경우

  • 경우에 따라 Annotation을 붙일 수 없는 경우도 있는데,
    • 다른 라이브러리에 있는 Enum이거나,
    • 붙이게 되면 항상 key-value 형식으로 변환되기 때문에 기존의 Code만으로 변환되는 것과 key-value 형식으로 변환되는 것을 선택해야 한다는 요구사항이 있을 때.
  • 그럴 때는 다음과 같이 BeanSerializer를 통해 변환하도록 유도하는 방식을 사용할 수 있다.
    • 사실 이렇게 library 헤집어서 쓰는걸 별로 좋아하지는 않지만, 요구사항이 있다면 어쩔 수 없다.
  • 처음부터 모든 enum에 JsonFormat을 붙이는 방법이 더 좋지 않을까 싶은게, 결국 이 방법은 컨트롤러에서 리턴할 때 EnumType 이 아니라, 직접 변환이 끝난 Map<String, Object>를 리턴할 수 밖에 없기 때문.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class ObjectMapperUtil {

  /**
    * 기본적으로 jackson은 Enum은 EnumSerializer를 사용해서 변환하며, 이렇게 변환한 결과는 enum {CODE}가 된다.
    * (e.g., CardCode.CCBC 변환 결과는 "CCBC" 가 된다.
    *
    * 그러나 @JsonFormat(shape = JsonFormat.Shape.OBJECT) 이 붙어 있는 enum이 넘어오면, BeanSerializer를 사용해서 변환하며,
    * 이렇게 변환한 결과는 {"field-key":"field-value"} 쌍이 된다.
    * 이 BeanSerializer는 일반적인 객체를 변환할 때 사용하는 serializer이므로, 변환 결과가 key-value가 되는 것이다.
    *
    * 해당 메서드는 @JsonFormat이 붙지 않은 Enum도 key-value 형식으로 변환해주는 ObjectMapper를 구현 및 제공한다.
    * 이를 위해 BeanSerializer를 직접 반환 받아 Custom Serializer로 등록하여 Enum 타입에 대해서도 BeanSerializer가 동작하도록 했다.
    * BeanSerializerFactory.findBeanSerializer(sp, javaType, beanDesc)는
    * BeanSerializer를 얻는 메서드 중 추상화 수준이 가장 높은 메서드이다.
    *
    * @param type
    * @return
    */
    public static ObjectMapper getEnumObjectMapper(Class<?> type) {
        ObjectMapper objectMapper = new ObjectMapper();

        BeanSerializerFactory sf = (BeanSerializerFactory)objectMapper.getSerializerFactory();
        SerializationConfig config = objectMapper.getSerializationConfig();
        SerializerProvider sp = objectMapper.getSerializerProviderInstance();

        JavaType javaType = config.constructType(type);
        BeanDescription beanDesc = config.introspect(javaType);
        ((BasicBeanDescription) beanDesc).removeProperty("declaringClass");

        JsonSerializer<Object> serializer;
        try {
            serializer = sf.findBeanSerializer(sp, javaType, beanDesc);
        } catch (JsonMappingException e) {
            throw new RuntimeException(e);
        }

        SimpleModule module = new SimpleModule();
        module.addSerializer(serializer);
        objectMapper.registerModule(module);

        return objectMapper;
    }
}
  • class 안에 있는 field의 타입이 Enum인 경우에도 동작하는데, 이런 Enum이 여러 개인 경우에는 serializer를 여러 개 등록해줘야 하므로 메서드를 좀 수정할 필요가 있음!

방법 2 ) Enum 클래스 내에서 메서드로 제공하고 싶은 경우

  • 그냥 map.put("koName", this.koName); 이런 식으로 해도 되지만, 아래는 reflection을 이용해 한방에 처리하는 코드.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Map<String, String> toMap() {
    return Arrays.stream(this.getDeclaringClass().getDeclaredFields())
        .filter(field -> !field.isSynthetic() && !field.isEnumConstant())
        .collect(Collectors.toMap(
            field -> field.getName(),
            field -> getFieldValue(field)));
}

private String getFieldValue(Field field) {
    try {
        return (String)field.get(this);
    } catch (IllegalAccessException e) {
        throw new RuntimeException(e);
    }
}

serialization/deserialization 시 Enum으로 바로 매핑 하도록 지원하는 애너테이션

1
2
3
4
public enum CyberSourceReasonCode {
    SUCCESS("100")
    private String code;
}
1
2
3
public class PayerAuthEnrollResponse {
    private CyberSourceReasonCode reasonCode;
}
  • 요렇게 되어 있는 상황에서, reasonCode로 들어오는 값이 SUCCESS이면 바로 Enum으로 매핑되지만, 100이나 475 같은 code 필드에 해당하는 값이라고 하면 바로 Enum으로 매핑되지 않는다
    • 이를 바로 Enum으로 매핑하기 위해서는 어떻게 해야할까?
  • code 필드에 해당하는 값을 수신했을 때 바로 Enum으로 매핑하기 위해서는 code 필드에 @JsonValue 를 붙여주면 된다. (deserialization)
  • Enum을 json 변환 했을 때도 code에 해당하는 값으로 변환된다. (serialization)

[!info] 원래 @JsonValue는 serialization 시에 어떤 값으로 할 것인지만 지정하고, deserialization 시에 어떤 값으로 받을지는@JsonCreator 를 사용해 지정해주어야 하지만, Enum의 경우에 한하여 deserialization 시에 어떤 값을 매핑에 사용할지도 지정한다. (docs 참고)

[!warning] kotlin 에서는 @get:JsonValue 사용해야 동작 한다. 참고

This post is licensed under CC BY 4.0 by the author.