본문 바로가기

코딩/Java, SpringBoot

[Jackson] java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class X / Map<-> Object : convertValue() / jackson always needs Getter!!

 

공모전 준비중인 MyOrder가 거의 개발을 끝냈다. 시연동영상을 제출해야하기 때문에 시연 동영상 시나리오를 짜고 마감전에 미리 시나리오 테스트를 진행해보려 한다. 근데 제목과 같이 자꾸 예외가 발생했다. 원인을 글 맨 밑의 링크에서 쉽게 찾을 수 있었다. 이 내용을 짧게 정리하고자 한다.

@Test
	public void scenarioTest() throws Exception {
		List<Demand> demands = testRepository.insertAll();
		
		//사용자 가져오기
		MvcResult mvcResult = mvc.perform(get("/get-test-customer"))
				.andExpect(status().isOk())
				.andExpect(jsonPath("$.jwt").exists())
				.andExpect(jsonPath("$.uuid").exists())
				.andReturn();
		
		String reponse = mvcResult.getResponse().getContentAsString();
		LoginResponseDTO cusLoginResponseDTO = Mapper.objectMapper.readValue(reponse, LoginResponseDTO.class);
		
		
//		가게 리스트 가져오기
		StoreListRequestDTO listRequestDTO = StoreListRequestDTO.builder()
				.location(PusanLocation.DONGLAE)
				.offset(0)
				.limit(6)
				.build();
		String s = Mapper.objectMapper.writeValueAsString(listRequestDTO);
		MvcResult mvcResult1 = mvc.perform(post("/store/list")
						.contentType(MediaType.APPLICATION_JSON)
						.content(s))
				.andExpect(status().isOk())
				.andExpect(jsonPath("$[0].name").value("솔루션 메이커"))
				.andReturn();
                
		String StoreListResponseDTOJSON = mvcResult1.getResponse().getContentAsString();
		List<StoreListResponseDTO> li = (List<StoreListResponseDTO>) Mapper.objectMapper.readValue(StoreListResponseDTOJSON, List.class);
		System.out.println("테스트 : "+li.get(0));
        System.out.println(li.getClass()); //class java.util.ArrayList
		System.out.println(li.get(0).getClass());//웃긴건 readValue(), li.get(0) 할 때가 아니라 여기서 .getClass()하면 예외남.

<원인과 해결책> 

Jackson은 serialize/deserialize JSON, XML 라이브러리이다. 

Mapper.objectMapper.readValue(json, List<StoreListResponseDTO>.class) 이렇게 코드를 작성할 수 없다. 그래서 Jackson은 어떤 List에 넣어야할지  알지 못한다.

예외메시지를 살펴 보자. java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class pnu.problemsolver.~ 이 말은 즉슨 LinkedHashMap을 내가 지정한 StoreListReponseDTO 타입으로 바꿀 수 없다는 뜻이다.

왜 LinkedHashMap일까? 타입이 명시되어 있지 않을 때 Jackson은 LinkedHashMap을 기본으로 사용한다. 그리고 readValue()에 List.class를 넘겼을 때 jack은 ArrayList를 클래스를 만드는 것을 위의 코드에서 확인할 수 있다. 즉 두 사실을 종합하면 jackson은 readValue()의 결과로 ArrayList<LinkedHashMap> 을 만들어 낸다. 이는 아래의 사진에서도 확인할 수 있다. 변수 li의 타입은 ArrayList이고 각각의 요소는 LinkHashMap이라고 되어있다.

 

이제 해결책을 알아보자. com.fasterxml.jackson.core.type.TypeReference<T>객체를 함께 넘겨주면 된다. 위의 코드를 아래로 바꿔주면 된다. List.class 대신 TypeReference객체를 넘긴다. 그럼 잘 동작함.

List<StoreListResponseDTO> li =  Mapper.objectMapper.readValue(StoreListResponseDTOJSON, new TypeReference<List<StoreListResponseDTO>>() {});
//또는 아래와 같이 타입을 생략할 수도 있다.
List<StoreListResponseDTO> li =  Mapper.objectMapper.readValue(StoreListResponseDTOJSON, new TypeReference<>() {});

근데 갑자기 든 생각이 Map을 사용자 정의 클래스로 바인딩 못시켜주나?? 할 수 있을 것 같은데... 그래서 찾아봤다... 당연히 된다. Map<->Object 양방향 가능하다.

주의할 점이 Object에는 getter가 필요하다. setter는 없어도 된다. 

Getter가 없을 때 아래와 같은 예외를 띄운다. 좀 직관적이진 않은듯.

No serializer found for class pnu.problemsolver.myorder.Tmp1 and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)

@Test
	public void test() throws JsonProcessingException {
		Tmp1 t = new Tmp1(1, 2);
		Map map1 = Mapper.objectMapper.convertValue(t, Map.class);
		System.out.println(map1);//{a=1, b=2}
		
		System.out.println(map1.getClass());//class java.util.LinkedHashMap
		
		HashMap<String, Object> map = new HashMap();
		map.put("a", 1);
		map.put("b", 2);
		System.out.println(map);//{a=1, b=2}
		
		Tmp1 tmp1 = Mapper.objectMapper.convertValue(map, Tmp1.class);
		System.out.println(tmp1);//Tmp1(a=1, b=2)
	}
	
	
}

@NoArgsConstructor
@Data//Getter가 필수이다. Getter없으면 예외 띄움
class Tmp1 {
	Integer a;
	Integer b;
	
	public Tmp1(int a, int b) {
		this.a = a;
		this.b = b;
	}
}

Map- Object는 변환가능하다. 그럼 왜 앞서 예외를 띄운 것일까? 우리는 Jackson의 convert()함수를 사용한 것이 아니라 java기본 캐스팅을 사용했다. 상속관계가 아니기 때문에 캐스팅 되지 않는 것은 당연함. 

 

아래의 링크에 여러 방법이 있으니 참조

https://www.baeldung.com/jackson-linkedhashmap-cannot-be-cast

 

Jackson: java.util.LinkedHashMap cannot be cast to X | Baeldung

Learn why the "java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to X" exception occurs and how to solve the problem

www.baeldung.com

<요약>

  • TypeReference 사용
  • CollectionType colType = objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, StoreListResponseDTO.class); 사용
  • objectMapper.convertValue()로 Map<->object변환해서 사용
  • Creating a Generic Deserialization Method //좋은 아이디어!! 읽어보자.
  • xml사용

 

아래는 jackson pdf라는데 언젠가 읽어보자.!!

 

Do+JSON+with+Jackson.pdf
0.59MB