Permalink
Browse files

Json support for Scala

  • Loading branch information...
1 parent a9c99f5 commit 63448578b15dcc7bf4806878c7b3aa4c74193af6 @erwan erwan committed Dec 1, 2011
@@ -4,7 +4,12 @@ import play.api.mvc._
import play.api.cache.Cache
import play.cache.{Cache=>JCache}
+import sjson.json.JsonSerialization._
+import models._
+import models.Protocol._
+
object Application extends Controller {
+
def index = Action {
import play.api.Play.current
Cache.set("hello","world")
@@ -16,6 +21,11 @@ object Application extends Controller {
def post = Action {
Ok(views.html.index("POST!"))
}
+
+ def json = Action {
+ Ok(tojson(User(1, "Sadek", List("tea"))))
+ }
+
def index_java_cache = Action {
import play.api.Play.current
JCache.set("hello","world")
@@ -28,4 +38,4 @@ object Application extends Controller {
}
}
-
+
@@ -0,0 +1,12 @@
+package models
+
+import sjson.json._
+import dispatch.json._
+import JsonSerialization._
+
+case class User(id: Long, name: String, favThings: List[String])
+
+object Protocol extends DefaultProtocol {
+ implicit val UserFormat: Format[User] = asProduct3("id", "name", "favThings")(User)(User.unapply(_).get)
+}
+
@@ -2,11 +2,11 @@
# This file defines all application routes (Higher priority routes first)
# ~~~~
-POST /post controllers.Application.post()
GET / controllers.Application.index()
+POST /post controllers.Application.post()
+GET /json controllers.Application.json()
GET /index_java_cache controllers.Application.index_java_cache()
# Map static resources from the /public folder to the /public path
GET /public/*file controllers.Assets.at(path="/public", file)
-
@@ -1,6 +1,7 @@
package test
import org.specs2.mutable._
import play.api.mvc._
+import play.api.JSON._
import play.api.libs.iteratee._
import play.api.libs.concurrent._
import org.openqa.selenium.htmlunit.HtmlUnitDriver
@@ -9,6 +10,9 @@ import com.ning.http.client.providers.netty.NettyResponse
import play.api.WS
import play.api.ws.Response
+import models._
+import models.Protocol._
+
object FunctionalSpec extends Specification {
"an Application" should {
@@ -21,7 +25,10 @@ object FunctionalSpec extends Specification {
resultGet must contain ("Hello world")
val resultPost: String = WS.url("http://localhost:9000/post").post().value match { case r: Redeemed[Response] => r.a.body }
resultPost must contain ("POST!")
-
+ val resultJson: User = WS.url("http://localhost:9000/json").get().value match {
+ case r: Redeemed[Response] => { r.a.json.as[User] }
+ }
+ resultJson must be equalTo(User(1, "Sadek", List("tea")))
}
}
}
@@ -0,0 +1,62 @@
+package play.api
+
+import dispatch.json.{ JsValue, JsObject, JsArray, JsString, JsNumber, JsNull }
+import sjson.json.{ Reads, Writes, Format }
+
+object JSON {
+
+ // Extend the JsValue and JsObject API for easier parsing
+
+ implicit def dispatchJsObject2playJsObject(jsobject: JsObject) = PlayJsObject(jsobject)
+ implicit def dispatchJsValue2playJsValue(jsvalue: JsValue) = PlayJsValue(jsvalue)
+
+ case class PlayJsValue(protected val value: JsValue) {
+ import scala.util.control.Exception.catching
+
+ /**
+ * Convert the JsValue to a scala object. T can be any type with a Reads[T] available.
+ * Throws an exception if the JsValue can not be converted into this type.
+ */
+ def as[T](implicit fjs: Reads[T]): T = asOpt[T].get
+
+ /**
+ * Convert the JsValue to a scala object. T can be any type with a Reads[T] available.
+ * Return None if the JsValue doesn't correspond to the expected type.
+ */
+ def asOpt[T](implicit fjs: Reads[T]): Option[T] = value match {
+ case JsNull => None
+ case _ => catching(classOf[RuntimeException]).opt(fjs.reads(value))
+ }
+
+ /**
+ * Supposes that the current JsValue is an object and tries to extract a given key.
+ * Throws if the JsValue is not an object.
+ */
+ def \(key: String): JsValue = value match {
+ case JsObject(m) => m(JsString(key))
+ case _ => throw new RuntimeException(value + " is not a JsObject")
+ }
@julienrf
julienrf Dec 1, 2011 Contributor

Why not move this method in PlayJsObject, in order to avoid the runtime exception?
But ok, it will be redundant with the apply method…
So, what’s the use case motivating this method?
I really think runtime exception are just traps that must be avoided at all prices, maybe this method should return an Option[JsValue]?

@guillaumebort
guillaumebort Dec 1, 2011 Collaborator

I think we need something more composable. Probably like NodeSeq.

@julienrf
julienrf Dec 1, 2011 Contributor

I find that lift-json querying capabilities are nice. They provide both a monadic API (map, flatMap and withFilter — and also some helpful unapply extractors) allowing to use high readable for-comprehension and a “XPath-like” API, similar to the one in NodeSeq.

@erwan
erwan Dec 1, 2011 Collaborator

Well, the idea is to be able to chain easily when you know where you're getting the info, e.g.:
"response" \ "user" \ "name" as[String]
If we put it in JsObject, you need to pattern match to get it from JsValue all the way down.

Anyway, we can definately improve it.

@pk11
pk11 Dec 1, 2011 Collaborator

One other thing to consider is https://github.com/codahale/jerkson/ -> this wraps around Jackson. I used this library in production before and I found it the best scala json lib out there. The other benefit of jerkson in play's context is that this would provide one dependency for both languages.

@pk11
pk11 Dec 1, 2011 Collaborator

(I would also add that both dispatch-json and liftweb-json are bringing in a few other dependencies)

@ikester
ikester Dec 1, 2011

I believe Jerkson/Jackson is reflexion based and I remember reading somewhere that the dev team wanted a Typeclass option.

By the way, I just tried the following and it throws a NPE:

package models

case class Company(id: Long, name: String, owner: User)
case class User(id: Long, name: String, favThings: List[String])

import sjson.json._
import dispatch.json._
import JsonSerialization._

object Protocol extends DefaultProtocol {
    implicit val CompanyFormat: Format[Company] = asProduct3("id", "name", "owner")(Company)(Company.unapply(_).get)
    implicit val UserFormat: Format[User] = asProduct3("id", "name", "things")(User)(User.unapply(_).get)
}

And in the controller:

  def json = Action {
    Ok(tojson(Company(1, "Sedutto", User(1, "Ikester", List("tea", "movies", "music")))))
  }
+
+ }
+
+ case class PlayJsObject(protected override val value: JsObject) extends PlayJsValue(value) {
+ def apply(key: String): JsValue = value.self(JsString(key))
+ def get(key: String): Option[JsValue] = value.self.get(JsString(key))
+ }
+
+ /**
+ * Helper for Json. Includes a simpler way to create heterogeneous JsObject or JsArray:
+ *
+ * jsobject("id" -> 1,
+ * "name" -> "Toto",
+ * "asd" -> jsobject("asdf" -> true),
+ * "tutu" -> jslist("asdf", 4, true))
+ *
+ * Any type T that has Format[T] implicit imported can be used.
+ */
+ def jsobject(params: (String, JsValue)*): JsObject =
+ JsObject(params.map(t => (JsString(t._1), t._2)))
+
+ def jsarray(params: JsValue*): JsArray = JsArray(params.toList)
+
+}
@@ -272,9 +272,16 @@ package ws {
class Response(ahcResponse: AHCResponse) extends WSResponse(ahcResponse) {
import scala.xml._
+ import dispatch.json.Js
lazy val xml = XML.loadString(body)
+ /**
+ * Return the body as a JsValue.
+ * Import play.libs.JSON._ and use asOpt[T] or as[T] to parse it to any object.
+ */
+ lazy val json = Js(body)
+
}
case class StreamedResponse(status: Int, headers: Map[String, Seq[String]], chunks: Enumerator[Array[Byte]])
@@ -290,6 +290,7 @@ trait Results {
import play.api.http.HeaderNames._
import play.api.http.ContentTypes._
import play.api.templates._
+ import dispatch.json.JsValue
/** `Writeable` for `Content` values. */
implicit def writeableOf_Content[C <: Content](implicit codec: Codec): Writeable[C] = {
@@ -306,6 +307,11 @@ trait Results {
Writeable[scala.xml.NodeBuffer](xml => codec.encode(xml.toString))
}
+ /** `Writeable` for `JsValue` values - Json */
+ implicit def writeableOf_JsValue(implicit codec: Codec): Writeable[dispatch.json.JsValue] = {
+ Writeable[dispatch.json.JsValue](jsval => codec.encode(jsval.toString))
+ }
+
/** `Writeable` for empty responses. */
implicit val writeableOf_Empty = Writeable[Results.Empty](_ => Array.empty)
@@ -319,6 +325,11 @@ trait Results {
ContentTypeOf[Xml](Some(XML))
}
+ /** Default content type for `JsValue` values (`application/json`). */
+ implicit def contentTypeOf_JsValue(implicit codec: Codec): ContentTypeOf[JsValue] = {
+ ContentTypeOf[JsValue](Some(JSON))
+ }
+
/** Default content type for `Txt` values (`text/plain`). */
implicit def contentTypeOf_Txt(implicit codec: Codec): ContentTypeOf[Txt] = {
ContentTypeOf[Txt](Some(TEXT))
@@ -441,4 +452,4 @@ trait Results {
*/
def Redirect(call: Call): SimpleResult[Results.Empty] = Redirect(call.url)
-}
+}
@@ -136,6 +136,7 @@ object PlayBuild extends Build {
"com.ning" % "async-http-client" % "1.6.5",
"oauth.signpost" % "signpost-core" % "1.2.1.1",
"org.reflections" % "reflections" % "0.9.5",
+ "net.debasishg" %% "sjson" % "0.15",
"javax.servlet" % "javax.servlet-api" % "3.0.1",
"org.specs2" %% "specs2" % "1.6.1" % "test",
"com.novocode" % "junit-interface" % "0.7" % "test",

0 comments on commit 6344857

Please sign in to comment.