Skip to content
This repository has been archived by the owner on Jan 18, 2024. It is now read-only.
Jong Wook Kim edited this page Feb 2, 2017 · 1 revision

Map, Seq 등의 자료구조나 case class를 다음과 같이 손쉽게 JSON으로 변환할 수 있습니다.

import com.kakao.mango.json._

case class Foo(hello: String)

toJson(Foo("world"))              // {"hello":"world"}
toJson(1 to 10)                   // [1,2,3,4,5,6,7,8,9,10]
toJson(Map("hello" -> "world"))   // {"hello":"world"}

parseJson 메소드를 사용하면 JSON을 Map[String, Any]타입으로 파싱합니다. 스키마가 미리 정해지지 않은 비정형 JSON을 처리할 수 있습니다.

val json = """{"a": 3, "b": true, "c": [1,2,3], "d": {"hello": "world"}}"""

val data: Map[String, Any] = parseJson(json)

data.foreach {
  case (key, value: Int) => println(s"Integer: $key => $value")
  case (key, value: Boolean) => println(s"Boolean: $key => $value")
  case (key, value: Seq[_]) => println(s"Seq: $key => $value")
  case (key, value: Map[_,_]) => println(s"Map: $key => $value")
}

위 코드는 parseJson이 리턴한 Map[String, Any]의 원소들을 패턴매칭을 통해 출력하는 예로, 결과는 다음과 같습니다.

Integer: a => 3
Boolean: b => true
Seq: c => List(1, 2, 3)
Map: d => Map(hello -> world)

주어진 타입으로 파싱하고 싶은 경우엔 fromJson[타입이름]으로 한 번에 파싱할 수 있습니다.

case class Foo(hello: String)

val json = """{"hello": "world"}"""
val foo = fromJson[Foo](json)     // Foo("world")

이외에도 다음과 같은 메소드를 지원합니다. 바이트 배열은 UTF-8 인코딩을 사용합니다.

메소드 리턴타입 용도
toJson(obj:Any) String obj를 JSON 문자열로 변환
toPrettyJson(obj:Any) String obj를 줄바꿈이 잘 된 JSON 문자열로 변환
serialize(obj:Any) Array[Byte] obj를 JSON 바이트 배열로 변환
serializePretty(obj:Any) Array[Byte] obj를 줄바꿈이 잘 된 JSON 바이트 배열로 변환
fromJson[T](src:Array[Byte]) T JSON 바이트 배열을 T 타입으로 파싱
fromJson[T](src:String) T JSON 문자열을 T타입으로 파싱
parseJson(src:Array[Byte]) Map[String,Any] JSON을 담은 UTF-8 바이트 배열을 Map으로 파싱
parseJson(src:String) Map[String,Any] JSON 문자열을 Map으로 파싱

숫자 파싱에 관해

Mango JSON을 사용하다 보면 아래와 같이 예기치 않은 오류가 발생할 수 있습니다.

import com.kakao.mango.json._

case class Test(m: Map[String, Double])

val test = fromJson[Test]("""{"m":{"answer":42}}""")

println(test.m("answer")) // 42 출력
println(test.m.get("answer").get) // 42 출력

val d = test.m("answer") // 여기서 ClassCastException: java.lang.Integer cannot be cast to java.lang.Double

test.m.get("answer") match {
  case Some(i) =>
    println(i) // 여기서 ClassCastException: java.lang.Integer cannot be cast to java.lang.Double
  case None => 
} 

Jackson 라이브러리와 JVM, 스칼라 언어의 스펙과 관련하여 다음과 같은 몇 가지 상황이 겹쳐서 발생하는 상황입니다.

  • JSON 스펙상에는 단 하나의 숫자 타입만 있으며, 길이나 정밀도에 대한 제한은 없습니다. * 하지만 JSON의 숫자를 파싱해서 JVM에서 다룰 때에는 여러 타입 중 하나를 사용해야 합니다.
  • 스칼라의 Int, Double과 같은 프리미티브 타입은 value: Double처럼 그냥 사용될 때는 자바의 프리미티브 타입으로, value: Option[Double]처럼 제너릭 파라미터로 사용될 때는 내부적으로 Object가 됩니다.
  • Jackson은 파싱할 숫자의 타입을 알고 있을 경우 그 타입으로, 그렇지 않거나 Object 타입이 주어져 있으면 숫자의 종류와 크기에 따라 Integer, Long, Float, Double, BigDecimal, BigInteger 중 하나의 자바 클래스로 파싱합니다.
  • 그래서 위 코드에서는 메모리상의 Map에 Integer타입의 오브젝트가 들어 있고, println의 경우 에러가 나지 않았지만 리턴타입을 변수로 할당하면 Double로 캐스팅이 일어나면서 에러가 발생했습니다.

Jackson 스칼라 모듈의 개발자도 이 현상에 대해서 많은 고민을 하였으며, @JsonDeserialize(contentAs = classOf[java.lang.Double])과 같은 어노테이션을 쓸 것을 추천하고 있습니다. 즉, 다음과 같이 하거나

import com.kakao.jackson.databind.annotation.JsonDeserialize

case class Test(
  @JsonDeserialize(contentAs = classOf[java.lang.Double])
  m: Map[String, Double]
)

아예 자바 타입을 쓸 수 있습니다.

case class Test(
  m: Map[String, java.lang.Double]
)

두 경우 모두 처음과 같은 에러는 발생하지 않습니다.

Clone this wiki locally