Scala/Play framework

Play Framework 2.5. Json 변환

partner_jun 2017. 4. 17. 15:18

어노테이션으로 객체를 간편하게 Json 문자열로 바꿀 수 있는 Spring과 달리 Play Framework는 객체를 Json으로 바꾸는 암시적 함수를 구현해 두어야한다.


1. 기본

먼저 Play Framework에서 사용되는 관련 구현체들을 이해할 필요가 있다.


1) JsValue

Json 값을 위한 Trait이다. JsValue는 암시적으로 선언된 Writeable 클래스를 통해 String으로 변환되므로 Action에서 Results.Ok(jsValue)와 같이 반환 할 수 있다. JsValue는 Json을 표현하기 위한 다양한 하위 자료형을 가지는데, JsValue를 상속해 구현된 타입은 아래와 같다.


 JsObject 

 "Key" -> "JsValue"를 가지고 있는 Map. 

 JsArray

 Json에서 []로 표기되는 JsValue 들을 담는 배열. 

 JsNull 

 Null

 JsBoolean

 true/false로 구분되는 boolean값.

 JsNumber

 Json의 숫자. BigDecimal 타입이다.

 JsString

 Json의 문자열.


Play Framework에서 Json은 JsValue를 상속해 만들어진 타입 객체들의 모임라고 볼 수 있다.


1-1) JsValue에서 값 얻어내기

JsValue는 \ 메소드나 \\ 메소드를 이용해 값을 스칼라 자료형으로 얻어 낼 수 있다. 

val data: JsValue = Json.obj(
"message" -> "Hello!",
"user" -> Json.obj("name" -> "jack",
"message" -> "hi")
)

val message: String = (data \ "message").as[String] // "Hello!"

val messageSeq: Seq[String] = (data \\ "message").map(v => v.toString()) // List("Hello!", "hi")

\\ 메소드는 "key"에 해당하는 value들을 모두 얻어낸다.


2) JsResult[T]

Play Framework의 타입 T의 객체로 변환이 '가능할수도' 있는 타입이다. Option 타입과 비슷하다. 변환이 가능하면 JsSuccess, 불가능하면 JsError를 가진다. 친숙한 고차함수들은 물론 get이나 getOrElse, orElse등의 함수가 구현되어 있고, asOpt 함수로 Option[T] 객체를 얻을 수도 있다.


3) Reads[T], Writes[T]

Object를 JsValue 타입으로 쓰거나, 반대로 JsValue를 Object로 읽을 때 사용되는 함수들이 구현되어있는 Trait이다. 


4) Json Object

Play Framework에서 Json과 관련된 처리를 하는 함수들이 구현되어 있다. 대표적인 함수 몇 가지만 적어본다.


 obj(key->value)

 "Key" -> "Value", "Key" -> Value" ... 형태로 입력받아 JsValue 타입 객체를 만든다.

parse(String)

 String을 파싱해 JsValue 타입을 반환한다.

 stringify(JsValue)

 JsValue를 String 타입으로 변환한다. JsValue의 toString() 함수로도 사용이 가능하다.

 prettyPrint(JsValue)

 JsValue를 값마다 개행된 '보기 편한' 문자열로 변환한다.

 toJson(Object : T)(implicit def Writes[T])

 Object를 암시적으로 선언된 T 타입의 Writes 객체를 반환하는 함수를 받아 JsValue 타입으로 변환한다.

 fromJson(JsValue)(implicit def Reads[T])

 toJson 함수와 반대로 JsValue를 암시적으로 선언된 T타입 Reads 객체를 반환하는 함수를 이용해 T 타입의 Object로 변환한다.




2. String to JsValue / JsValue to String

위에서 소개한 함수들을 이용해 간단하게 변환 할 수 있다.

val string = """
|{
| "number" : 1,
| "name" : "jack"
|}
""".stripMargin

val jsValue: JsValue = Json.parse(string)
val data: JsValue = Json.obj(
"message" -> "Hello!",
"name" -> "jack"
)

String to JsValue


val jsValue: JsValue = Json.obj(
// 1, "jack"처럼 scala 기본 타입을 사용해도 자동 변환된다.
"number" -> JsNumber(1),
"name" -> JsString("jack")
)

val string: String = jsValue.toString() // Json.stringify(jsValue)와 같음.

JsValue to String




3. JsValue to Class / Class to JsValue

먼저 Writes[T]와 Reads[T]를 반환하는 암시적 함수를 선언하고 구현한다.

import play.api.libs.json._
import play.api.libs.functional.syntax._

case class User(number: Int, name: String)

implicit def userRead: Reads[User] = (
(JsPath \ "number").read[Int] and
(JsPath \ "name").read[String]
)(User.apply _)

implicit def userWrites: OWrites[User] = (
(JsPath \ "number").write[Int] and
(JsPath \ "name").write[String]
)(unlift(User.unapply))

Reads[User]와 Writes[User]를 반환하는 함수 (OWrites는 JsObject를 반환한다)


JsPath Object에서 Key를 \ 함수로 분리하고 자료형에 일치시킨다. 클래스가 Option 값을 가진다면 readNullable, 클래스와 같은 자료형을 가지는 재귀적 형태라면 lazyRead 함수를 사용한다.


val user = User(1, "jack")
val userJsValue: JsValue = Json.toJson(user)

Class to JsValue


val userJsResult: JsResult[User] = Json.fromJson(userJsValue)
val userOption: Option[User] = userJsResult.asOpt

JsValue to JsResult



4. Json안에 Json Object가 있는 형태

Json 안에 Json Object가 포함된 경우에는 fromJson 함수에 Reads[T]를 반환하는 함수를 직접 입력해야 한다.

import play.api.libs.functional.syntax._
import play.api.libs.json._

case class User(number: Int, name: String)

implicit def userRead: Reads[User] = (
(JsPath \ "number").read[Int] and
(JsPath \ "name").read[String]
)(User.apply _)
implicit def userWrite: OWrites[User] = (
(JsPath \ "number").write[Int] and
(JsPath \ "name").write[String]
)(unlift(User.unapply))


case class Article(title: String, user: User)

implicit def articleRead: Reads[Article] = (
(JsPath \ "title").read[String] and
(JsPath \ "user").read[User]
)(Article.apply _)
implicit def articleWrite: OWrites[Article] = (
(JsPath \ "title").write[String] and
(JsPath \ "user").write[User]
)(unlift(Article.unapply))


case class Board(article: Article)
implicit def boardRead: Reads[Board] = (JsPath \ "article").read[Article].map(Board.apply)
implicit def boardWrite = (JsPath \ "article").write[Article].contramap{(board: Board) => board.article}

Board(Article(User)) 형태의 클래스




Request body가 Board 클래스의 Json일 때(크롬 앱 Advanced Rest Client 사용)


val board: Board = Board(Article("Hello", User(2, "duck")))
val boardJsValue: JsValue = Json.toJson(board)
val boardJsValue: JsValue = req.body.asJson.get
val board: JsResult[Board] = Json.fromJson(boardJsValue)(boardRead) // boardRead 함수를 직접 전달해야 한다.