Permalink
Browse files

JSON: Switch to jackson/jerkson, and include typeclasses serializatio…

…n/deserialization. Still to do our own AST (Json API still going to change)
  • Loading branch information...
1 parent 4bbcf27 commit d292fd30dfd6534bb87f37e56577832063608205 @erwan erwan committed Dec 2, 2011
@@ -4,7 +4,7 @@ import play.api.mvc._
import play.api.cache.Cache
import play.cache.{Cache=>JCache}
-import sjson.json.JsonSerialization._
+import play.api.Json._
import models._
import models.Protocol._
@@ -1,12 +1,27 @@
package models
-import sjson.json._
-import dispatch.json._
-import JsonSerialization._
+import play.api.Json._
+import com.codahale.jerkson.AST._
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)
+object Protocol {
+
+ implicit object UserFormat extends Format[User] {
+
+ def writes(o: User): JValue = JObject(
+ List(JField("id", JInt(o.id)),
+ JField("name", JString(o.name)),
+ JField("favThings", JArray(o.favThings.map(JString(_))))
+ ))
+
+ def reads(json: JValue): User = User(
+ fromjson[Long](json \ "id"),
+ fromjson[String](json \ "name"),
+ fromjson[List[String]](json \ "favThings")
+ )
+
+ }
+
}
@@ -26,7 +26,7 @@ object FunctionalSpec extends Specification {
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] }
+ case r: Redeemed[Response] => fromjson[User](r.a.json)
}
resultJson must be equalTo(User(1, "Sadek", List("tea")))
}
@@ -2,12 +2,6 @@
import com.ning.http.client.AsyncHttpClient;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonParser;
-import com.google.gson.JsonDeserializer;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-
import java.lang.reflect.Method;
import java.lang.reflect.Type;
@@ -17,6 +11,9 @@
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
+import org.codehaus.jackson.JsonNode;
+import org.codehaus.jackson.map.ObjectMapper;
+
public class WS {
private static AsyncHttpClient client = play.api.WS.client();
@@ -68,38 +65,17 @@ public Document asXml() {
}
/**
- * Get the response body as a {@link com.google.gson.JsonElement}
+ * Get the response body as a {@link org.codehaus.jackson.JsonNode}
* @return the json response
*/
- public JsonElement asJson() {
+ public JsonNode asJson() {
String json = getBody();
+ ObjectMapper mapper = new ObjectMapper();
try {
- return new JsonParser().parse(json);
+ return mapper.readValue(json, JsonNode.class);
} catch (Exception e) {
- Logger.error("Bad JSON: " + json, e);
- throw new RuntimeException("Cannot parse JSON (check logs)", e);
- }
- }
-
- /**
- * Get the response body as a Json, and to convert it in a business class object
- * @param classOfT the target class to deserialize the Json
- */
- public <T> T fromJson(Class<T> classOfT) {
- return (new Gson()).fromJson(getBody(), classOfT);
- }
-
- /**
- * Get the response body as a Json, and to convert it in a business class object
- * @param adapters custom deserializers to be used
- */
- public <T> T fromJson(Class<T> classOfT, JsonDeserializer<?>... adapters) {
- GsonBuilder gson = new GsonBuilder();
- for (Object adapter: adapters) {
- Type t = getMethod(adapter.getClass(), "deserialize").getParameterTypes()[0];
- gson.registerTypeAdapter(t, adapter);
+ throw new RuntimeException(e);
}
- return gson.create().fromJson(getBody(), classOfT);
}
private static Method getMethod(Class<?> clazz, String name) {
@@ -1,110 +1,111 @@
package play.api
-import dispatch.json.{ JsValue, JsObject, JsArray, JsString, JsNumber, JsNull }
-import sjson.json.{ Reads, Writes, Format }
+import com.codahale.jerkson.AST._
object Json {
- // Extend the JsValue and JsObject API for easier parsing
+ def parse(input: String): JValue = com.codahale.jerkson.Json.parse[JValue](input)
- implicit def dispatchJsValue2playJsValue(jsvalue: JsValue) = PlayJsValue(jsvalue)
+ def tojson[T](o: T)(implicit tjs: Writes[T]): JValue = tjs.writes(o)
- sealed trait PlayJson {
- import scala.util.control.Exception.catching
+ def fromjson[T](json: JValue)(implicit fjs: Reads[T]): T = fjs.reads(json)
- /**
- * 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
+ trait Writes[T] {
+ def writes(o: T): JValue
+ }
- /**
- * 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]
+ trait Reads[T] {
+ def reads(json: JValue): T
+ }
- /**
- * 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): PlayJson
+ trait Format[T] extends Writes[T] with Reads[T]
- def asValue: JsValue = this match {
- case PlayJsValue(v) => v
- case u @ PlayJsUndefined(msg) => throw new Exception(msg)
+ implicit object IntFormat extends Format[Int] {
+ def writes(o: Int) = JInt(o)
+ def reads(json: JValue) = json match {
+ case JInt(n) => n.toInt
+ case _ => throw new RuntimeException("Int expected")
}
+ }
- def toEither: Either[String, JsValue] = this match {
- case PlayJsValue(v) => Right(v)
- case u @ PlayJsUndefined(msg) => Left(msg)
+ implicit object ShortFormat extends Format[Short] {
+ def writes(o: Short) = JInt(o)
+ def reads(json: JValue) = json match {
+ case JInt(n) => n.toShort
+ case _ => throw new RuntimeException("Short expected")
}
}
- case class PlayJsValue(protected val value: JsValue) extends PlayJson {
- 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 = fjs.reads(value)
-
- /**
- * 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 value => catching(classOf[RuntimeException]).opt(fjs.reads(value))
+ implicit object LongFormat extends Format[Long] {
+ def writes(o: Long) = JInt(o)
+ def reads(json: JValue) = json match {
+ case JInt(n) => n.toLong
+ case _ => throw new RuntimeException("Long expected")
}
+ }
- /**
- * 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): PlayJson = value match {
- case JsObject(m) => m.get(JsString(key)).map(PlayJsValue(_)).getOrElse(PlayJsUndefined("'" + key + "'" + " is undefined on object: " + value))
- case other => PlayJsUndefined(key + " is undefined on value: " + value)
+ implicit object FloatFormat extends Format[Float] {
+ def writes(o: Float) = JFloat(o)
+ def reads(json: JValue) = json match {
+ case JFloat(n) => n.toFloat
+ case _ => throw new RuntimeException("Float expected")
}
+ }
+ implicit object DoubleFormat extends Format[Double] {
+ def writes(o: Double) = JFloat(o)
+ def reads(json: JValue) = json match {
+ case JInt(n) => n.toDouble
+ case _ => throw new RuntimeException("Double expected")
+ }
}
- case class PlayJsUndefined(error: String) extends PlayJson {
+ implicit object BooleanFormat extends Format[Boolean] {
+ def writes(o: Boolean) = JBoolean(o)
+ def reads(json: JValue) = json match {
+ case JBoolean(b) => b
+ case _ => throw new RuntimeException("Boolean expected")
+ }
+ }
- /**
- * 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 = throw new Exception("undefined: " + error + ", cannot be viewed as the required type")
+ implicit object StringFormat extends Format[String] {
+ def writes(o: String) = JString(o)
+ def reads(json: JValue) = json match {
+ case JString(s) => s
+ case _ => throw new RuntimeException("String expected")
+ }
+ }
- /**
- * 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] = None
+ implicit def listFormat[T](implicit fmt: Format[T]): Format[List[T]] = new Format[List[T]] {
+ def writes(ts: List[T]) = JArray(ts.map(t => tojson(t)(fmt)))
+ def reads(json: JValue) = json match {
+ case JArray(ts) => ts.map(t => fromjson(t)(fmt))
+ case _ => throw new RuntimeException("List expected")
+ }
+ }
- /**
- * 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): PlayJson = this
+ implicit def seqFormat[T](implicit fmt: Format[T]): Format[Seq[T]] = new Format[Seq[T]] {
+ def writes(ts: Seq[T]) = JArray(ts.toList.map(t => tojson(t)(fmt)))
+ def reads(json: JValue) = json match {
+ case JArray(ts) => ts.map(t => fromjson(t)(fmt))
+ case _ => throw new RuntimeException("Seq expected")
+ }
+ }
+ import scala.reflect.Manifest
+ implicit def arrayFormat[T](implicit fmt: Format[T], mf: Manifest[T]): Format[Array[T]] = new Format[Array[T]] {
+ def writes(ts: Array[T]) = JArray((ts.map(t => tojson(t)(fmt))).toList)
+ def reads(json: JValue) = json match {
+ case JArray(ts) => listToArray(ts.map(t => fromjson(t)(fmt)))
+ case _ => throw new RuntimeException("Array expected")
+ }
}
+ def listToArray[T: Manifest](ls: List[T]): Array[T] = ls.toArray
- /**
- * 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)
+ implicit object JValueFormat extends Format[JValue] {
+ def writes(o: JValue) = o
+ def reads(json: JValue) = json
+ }
}
+
@@ -272,15 +272,13 @@ 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.
+ * Return the body as a JValue.
*/
- lazy val json = Js(body)
+ lazy val json = play.api.Json.parse(body)
}
@@ -290,7 +290,7 @@ trait Results {
import play.api.http.HeaderNames._
import play.api.http.ContentTypes
import play.api.templates._
- import dispatch.json.JsValue
+ import com.codahale.jerkson.AST.JValue
/** `Writeable` for `Content` values. */
implicit def writeableOf_Content[C <: Content](implicit codec: Codec): Writeable[C] = {
@@ -307,9 +307,9 @@ 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 `JValue` values - Json */
+ implicit def writeableOf_JValue(implicit codec: Codec): Writeable[JValue] = {
+ Writeable[JValue](jsval => codec.encode(com.codahale.jerkson.Json.generate(jsval)))
}
/** `Writeable` for empty responses. */
@@ -325,9 +325,9 @@ trait Results {
ContentTypeOf[Xml](Some(ContentTypes.XML))
}
- /** Default content type for `JsValue` values (`application/json`). */
- implicit def contentTypeOf_JsValue(implicit codec: Codec): ContentTypeOf[JsValue] = {
- ContentTypeOf[JsValue](Some(ContentTypes.JSON))
+ /** Default content type for `JValue` values (`application/json`). */
+ implicit def contentTypeOf_JValue(implicit codec: Codec): ContentTypeOf[JValue] = {
+ ContentTypeOf[JValue](Some(ContentTypes.JSON))
}
/** Default content type for `Txt` values (`text/plain`). */
Oops, something went wrong.

3 comments on commit d292fd3

@ikester
ikester commented on d292fd3 Dec 3, 2011

What are the pros and cons of doing all of this vs. letting Jerkson take care of it by doing:

Ok(generate(CaseClass("blah")))

Unless you really need to control the parsing and serialization, do you see any benefit in declaring an implicit Format object?

@erwan
Collaborator
erwan commented on d292fd3 Dec 3, 2011

The major benefit of using typeclasses is that if your code compiles, you have the guarantee that it will work. Letting Jerkson do introspection means potential runtime error if it fails to serialize your object.

@ikester
ikester commented on d292fd3 Dec 5, 2011

Thanks. Performance-wise I couldn't see any difference in my primitive performance test.

Please sign in to comment.