JSON is a common data interchange format for semistructured data. It’s human-readable and supports primitive, array, and object values. Additionally, for web services it’s the natural data format of the browser, due to its close relationship with JavaScript.
Scalatra offers a module that supports an application working with JSON. This chapter covers its use with practical examples taken from the food domain. Who knows, maybe you’ll also learn a useful recipe!
The Scalatra JSON module extends an application’s request handling with two facets:
Let’s see how you can add JSON support to an application.
Because Scalatra’s JSON support is an optional module, it needs to be added as a dependency to the sbt build definition:
libraryDependencies ++= Seq( "org.scalatra" %% "scalatra-json" % ScalatraVersion, "org.json4s" %% "json4s-jackson" % "3.3.0")
Those dependencies pull in scalatra-json and Json4s. Json4s is a Scala JSON library; scalatra-json is the actual JSON module that builds on top of Json4s, reusing its JSON-handling methods and JValue data type. This chapter covers a fair amount of Json4s, but for full documentation refer to the official website at http://json4s.org.
Let’s look at what an application needs to do in order to handle JSON. Listing 5.1 shows a minimal example application defining two routes in a trait. You’ll need to mix these into a class in order to run them.
The GET route builds and returns a JSON document representing a delicious snack. The POST route extracts a tuple out of a JSON request and prints it to the console.
Let’s take a closer look at what happens in this listing. The JacksonJsonSupport trait is responsible for parsing an incoming JSON request to a JValue as well as for writing a result of type JValue as JSON text to a response. This is accomplished by hooking into Scalatra’s request-response cycle.
The GET route returns a JValue as a result, which is written as JSON text to the response. Generally, if you intend to output JSON using Scalatra’s JSON support, your JSON routes should always return a value of type JValue. When the route result is of type JValue, it’s set implicitly to application/json by JacksonJsonSupport.
Because an HTTP request contains the JSON in textual form, JacksonJsonSupport provides the function parsedBody, which parses the JSON text and returns a value of type JValue. parsedBody does several things:
The request will be parsed eagerly before the route is invoked. With the JSON parsed from the request, the next logical step is usually to extract useful information from the JSON data. This can include the following:
You may have noticed the implicit jsonFormats value. It holds serialization configuration telling Json4s how to handle specific cases when parsing and writing JSON. For instance, a JSON number can either be handled as a Double or a BigDecimal. The JSON support requires you to define this value.
The Formats type is explained in detail in section 5.3.1, and we’ll look at constructing and working with JSON in more detail in section 5.2. For now we’ll stay with the DefaultFormats settings. As a rule, you should always return a JValue from an action and use the parsedBody method to access the request’s JSON.
Let’s now take a closer look at the JValue type.
The JValue type represents a JSON intermediate model. You can think of a value of type JValue as the abstract representation of a JSON document, often called its abstract syntax tree. This simplifies further read and modify operations on that JSON data.
JSON is a typed language, so a JValue needs to support all types that may appear in a JSON document. Those are objects, arrays, and primitive values (such as string, number, Boolean, and null). For each JSON type, there’s a counterpart in the intermediate model. Listing 5.2 shows the various types. Note that a JValue boxes a native Scala value—it acts as a container for a value of that type.
sealed trait JValue case class JString(s: String) extends JValue case class JBool(value: Boolean) extends JValue trait JNumber case class JInt(num: BigInt) extends JValue with JNumber case class JDouble(num: Double) extends JValue with JNumber case class JDecimal(num: BigDecimal) extends JValue with JNumber type JField = (String, JValue) case class JObject(obj: List[JField]) extends JValue case class JArray(arr: List[JValue]) extends JValue case object JNothing extends JValue case object JNull extends JValue
Let’s look at how a JSON document is represented as a JValue. The following listing shows a sample JSON document.
{ "label" : "Foo bar", "fairTrade" : true, "tags" : [ "bio", "chocolate" ] }
Figure 5.1 shows how the document looks as a JValue. A JSON document forms a tree. The top-level object is a JObject containing a set of key-value pairs representing the JSON object. The keys (label, tags, and fairTrade) are of type String. The values are of type JValue.
A document tree can be arbitrarily deep. The array at the key tags is represented as a value of type JArray.
Constructing this document from scratch using the JValue types is straightforward. Let’s construct some objects and see what the resulting Scala types look like. You can play around with all of the JSON code in a Scala console. Result types are shown in comments below the assignment code.
val fooBar = JObject( "label" -> JString("Foo bar"), "fairTrade" -> JBool(true), "tags" -> JArray(List(JString("bio"), JString("chocolate")))) // fooBar: org.json4s.JValue = // JObject(List((label,JString(Foo bar)), ...
The JValue can also be parsed from JSON text using the parse method, which is defined on the JsonMethods object:
import org.json4s.jackson.JsonMethods.parse val txt = """{ | "tags": ["bio","chocolate"], | "label": "Foo bar", | "fairTrade": true |}""".stripMargin val parsed = parse(txt) // parsed: org.json4s.JValue = // JObject(List((label,JString(Foo bar)), ...
When comparing the previously constructed value against the parsed value, they are equal. Note that the order of the fields in a JObject doesn’t matter for equality:
fooBar == parsed // res: Boolean = true
You should now have a basic understanding of the scope of Scalatra’s JSON support. Remember that parsing and serialization is done by the JSON support transparently in a request-response cycle. Also note that it is easy to integrate another JSON library on your own, such as argonaut or play-json.
Now let’s look at how you can work with JSON data.
When receiving JSON, you’ll probably want to be able to extract useful information. When sending JSON, you’ll need to be able to construct it. Let’s start with getting to know the JSON intermediate data model. We’ll serialize a Recipe object instance as an example.
Applications use JSON to encode data that’s delivered to the client, so the application needs to be able to create a JSON message. In this section, you’ll learn a simple but effective DSL for constructing a JSON value that’s then delivered to a client as the body of an HTTP response.
There are two ways to create JSON, which can also be combined:
When your application receives a JSON request, it needs to be able to interpret the JSON contained in that request. This section will introduce you to the following:
The following listing shows a recipe for an Italian pasta dish as a JSON document. We’ll use this JSON document to explain the concepts in this section.
{ "title": "Penne with cocktail tomatoes, Rucola and Goat cheese", "details": { "cuisine": "italian", "vegetarian": true }, "ingredients": [{ "label": "Penne", "quantity": "250g" }, { "label": "Cocktail tomatoes", "quantity": "300g" }, { "label": "Rucola", "quantity": "2 handful" }, { "label": "Goat cheese", "quantity": "200g" }, { "label": "Garlic cloves", "quantity": "2 tsps" }], "steps": [ "Cook noodles until aldente.", "Quarter the tomatoes, wash the rucola, dice the goat's cheese and cut the garlic.", "Heat olive oil in a pan, add the garlic and the tomatoes and steam short (approx. for 5 minutes).", "Shortly before the noodles are ready add the rucola to the tomatoes.", "Drain the noodles and mix with the tomatoes, finally add the goat's cheese and serve." ] }
Although it’s possible to stick with the JValue types when processing JSON, it’s often helpful or necessary to also employ classes. The JSON language features basic types, but classes give you additional expressivity and type safety. In addition, other third-party libraries, such as database-mapping layers, may require the use of classes.
The domain model consists of the types Recipe, RecipeDetails, and IngredientLine. The code is shown in the following listing.
case class Recipe(title: String, details: RecipeDetails, ingredients: List[IngredientLine], steps: List[String]) case class RecipeDetails(cuisine: String, vegetarian: Boolean, diet: Option[String]) case class IngredientLine(label: String, quantity: String)
There are three methods of creating a JValue in the Json4s library:
Creating a JValue from scratch using the JValue types has the benefit that you can be explicit about the resulting JSON. But it’s also the most verbose approach and can lead to cumbersome code. You saw this approach in section 5.1.2, so we’ll employ the other two approaches here.
The DSL is based on a few simple operators and implicit conversion. In order to use the JSON DSL, it needs to be made available in the current scope by importing org.json4s.JsonDSL._. This enables conversion from primitive and collection types to a JValue by specifying the expected type for an expression:
val jsString: JValue = "italian" // jsString: org.json4s.JValue = JString(italian) val jsBool: JValue = true // jsBool: org.json4s.JValue = JBool(true)
A JSON object is constructed from Tuple2[String, A], where A has an implicit view to JValue. The ~ operator combines multiple fields into a single JSON object. JSON arrays are created from Scala collections:
val detailsJson = ("cuisine" -> "italian") ~ ("vegetarian" -> true) // detailsJson: org.json4s.JsonAST.JObject = // JObject(List((cuisine,JString(italian)), // (vegetarian,JBool(true)))) val tags: JValue = List("higher", "cuisine")
The next listing shows an example using nested objects, a list of primitives, and a list of nested objects, and constructs the JSON document from listing 5.4.
val recipeJson = ("title" -> "Penne with cocktail tomatoes, Rucola and Goat cheese") ~ ("details" -> detailsJson) ~ ("ingredients" -> List( ("label" -> "Penne") ~ ( "quantity" -> "250g"), ("label" -> "Cocktail tomatoes") ~ ( "quantity" -> "300g"), ("label" -> "Rucola") ~ ( "quantity" -> "2 handful"), ("label" -> "Goat cheese") ~ ( "quantity" -> "250g"), ("label" -> "Garlic cloves") ~ ( "quantity" -> "250g"))) ~ ("steps" -> List( "Cook noodles until aldente.", "Quarter the tomatoes, wash the rucola, dice the goat's cheese ...", "Heat olive oil in a pan, add the garlic and the tomatoes and ...", "Shortly before the noodles are ready add the rucola to the ...", "Drain the noodles and mix with the tomatoes, finally add the ..."))
One goal of the DSL is to construct a valid JValue with very little boilerplate code. Because the DSL is based on implicit conversion, this means a conversion function needs to be available for all values.
Say you want to support a class from the recipe domain model shown earlier, in listing 5.5. Assume that you want to convert a RecipeDetails value to a JValue. When trying to use a value of type RecipeDetails directly in the DSL, this won’t work:
val jsObject: JValue = ("details" -> RecipeDetails("italian", true, None)) // <console>:23: error: No implicit view available // from RecipeDetails => org.json4s.JsonAST.JValue.
The error tells you that no conversion function is available in the current scope. Now let’s see how you can extend the DSL for your own types by providing one. The implicit function details2jvalue in the following listing takes a RecipeDetails and returns a JValue. This allows you to use a RecipeDetails.
implicit val formats = DefaultFormats implicit def details2jvalue(rd: RecipeDetails): JValue = Extraction.decompose(rd) val jsObject: JValue = ("details" -> RecipeDetails("italian", true, None)) // jsObject: org.json4s.JValue = // JObject(List( // (details,JObject(List((cuisine,JString(italian)), // (vegetarian,JBool(true)))))))
The function relies on decomposition to convert a RecipeDetails to a JValue. Decomposing is a generic reflection-based method. Depending on the concrete type of a value, either a JObject, a JArray, or a primitive value is constructed. It’s exposed through the function Extraction.decompose(a: Any): JValue.
Generally, the following rules are used:
It’s possible to override the default conversion behavior for a type by registering a custom serializer for that type. This is discussed in section 5.3.
The JValue DSL leads to concise code and allows you to produce JSON with little typing overhead. Decomposition doesn’t involve writing any manual conversion code, and it’s good for the average conversion case. It relies on general conventions that may not be suitable in all cases, so you may need to write a custom serializer.
Both approaches can be combined: decomposition can be used in the DSL, and the DSL can be used to write a custom serializer. Now let’s move on and look at how to process JSON.
In this section, you’ll learn how to traverse a JSON document and extract values from it. Let’s say you’re interested in a specific part of the JSON document, such as the title of the recipe in listing 5.4. In order to read this information, you need to be able to address that part of the document. Because a JSON object consists of nested key-value pairs, the keys can be used to perform a traversal trough the JSON document.
For example, selecting the value of the field with the name "title" works like this:
recipeJson "title" // res: JString(Penne with cocktail tomatoes, Rucola and Goat cheese)
The function (nameToFind: String): JValue represents a one-step select that tries to find a field with the given key in the current value. Multiple operations can be chained consecutively:
recipeJson "details" "cuisine" // res: JString(italian)
This one-step selection process allows you to select values deep in nested objects. The syntax resembles XPath in the XML world, which is used to address parts of an XML document.
In an array, the select operation is applied to each contained value, and the results of these invocations are merged in a single JArray:
recipeJson "ingredients" "label" // res: JArray(List(JString(Penne), JString(Cocktail tomatoes), ...))
In addition to simple selects, it’s possible to do a recursive selection that searches all nested values. This is what the function \(nameToFind: String): JValue does:
scala> recipeJson \ "cuisine" // res: org.json4s.JValue = JString(italian)
There are two special cases that we’ll take a short look at now: one regarding missing values and the other regarding null values.
Missing values happen when a traversal can’t find a suitable value in the JSON document. A missing value is represented by the JValue zero value, JNothing:
recipeJson "details" "prerequisites" // res: JNothing // there are no prerequisites, returns JNothing
Applying a select on a JNothing yields JNothing again.
A null value is often used to imply a missing value. It’s represented as the unique value JNull.
Now that you know how to navigate through a JSON document, let’s look at how you can extract a value of some arbitrary type, A, from a JValue. This is called extraction, and it’s handled by the function extract[A], which is defined on a JValue.
The following types are valid targets to extract to:
When extracting a value from a JValue, that JValue is checked for compatibility with the target type. When the two are compatible, an instance of the target type is constructed. Primitives and arrays are compatible with Scala’s primitives and collections. For example, the title of the recipe can be extracted as a String by calling extract with String as the target type:
(recipeJson "title").extract[String] // res: String = Penne with cocktail tomatoes, Rucola and Goat cheese
Collections are read from a JSON array. A list of preparation steps from the recipe can be extracted to a List[String]:
(recipeJson "steps").extract[List[String]] // res: List[String] = List(Cook noodles until aldente., ...)
Class instances are extracted from JSON objects using a 1:1 mapping approach between a JSON object and a class. For each constructor parameter of the class type, a matching field in the object is required. This is similar to decomposition, described in section 5.2.2.
For example, the following code extracts a RecipeDetails value:
(recipeJson "details").extract[RecipeDetails] // res: RecipeDetails = RecipeDetails(italian,true,None)
This is how extraction works. Note that because there is no diet in the JSON, a None is extracted.
The extraction fails at runtime when the JSON is incompatible with the type. For example, a JBool can’t be extracted as a String, and a missing value is incompatible with every type. In this case an exception is raised:
JNothing.extract[String] // MappingException: // Did not find value which can be converted into java.lang.String
In the case where an extraction may fail, the value can optionally be extracted. This means the value is read into an instance of Option[A]. This is what extractOpt[A] (json: JValue): Option[A] does, with the following possible results:
The following code shows various examples of optional extraction. Note that JNull and JNothing are both handled as missing values:
JString("foo").extractOpt[String] // res: Option[String] = Some(foo) JString("foo").extractOpt[Boolean] // res: Option[Boolean] = None JNull.extractOpt[String] // res: Option[String] = None JNothing.extractOpt[String] // res: Option[String] = None
The optional extraction can be combined with the / type from Scalaz (see https://github.com/scalaz/scalaz), resulting in a simple validation. Listing 5.8 shows how this can be done. The computation succeeds with an Ok(..) when all values are present. Otherwise, a BadRequest() is returned.
This should give you some methods for extracting basic information from JSON. Let’s now look at how to handle more-specific cases.
The conversion presented in the previous section is sufficient in cases where the structures of a class and a JSON object are very similar. In reality, there’s often some form of discrepancy. This can be a simple variation in the naming of fields or a difference in the structures.
Sometimes you can adjust one side so that both match, but in some cases this may not be feasible. For example, when you’re working with an existing data model, a code refactoring would be impossible due to dependencies. Similarly, when you’re supporting a standardized JSON format, designing Scala classes to exactly match the JSON format can be impractical.
In these cases, a custom conversion function is a cheaper and more sensible alternative. This section covers common approaches for handling such cases.
In Json4s, an implicit value of type Formats determines the concrete JSON handling. When mixing in the Scalatra JSON support, the implicit value needs to be defined, as shown earlier in listing 5.1.
There’s an implementation with common default values in the DefaultFormats trait. The trait defines properties that can be overridden when the default behavior isn’t sufficient. These are a few use cases where a custom Formats saves the day:
Table 5.1 lists properties that can be overridden. For a full reference, see the official Json4s documentation at http://json4s.org.
Formats property |
Explanation |
Default |
---|---|---|
wantsBigDecimal: Boolean | If true, a decimal value is read to JDecimal when decomposing. Otherwise, it’s read to a JDouble. | false |
strictOptionParsing: Boolean | If true, parsing to an Option[T] is strict. Incompatible values are then not mapped to None but throw an error. | false |
emptyValueStrategy: EmptyValueStrategy | When set to skip, a None is skipped. preserve writes None as null. | skip |
dateFormatter: SimpleDateFormat | Specifies the format used to parse and write dates. | yyyy-MM-dd'T' HH:mm:ss'Z' |
typeHints: TypeHints | Specifies what type hints variant should be used. | NoTypeHints |
typeHintsFieldName: String | Sets the name used as the key of the type hint field. | jsonClass |
customSerializers: List[Serializer[_]] | Specifies a list of custom serializers. | Empty list |
customKeySerializers: List[KeySerializer[_]] | Specifies a list of custom key serializers. | Empty list |
fieldSerializers: List[(Class[_], FieldSerializer[_])] | Specifies a list of field serializers. | Empty list |
Let’s see how the default formats handle dates. You’ll define an implicit Default-Formats value and decompose a map with a java.util.Date. After that, you’ll parse it again and extract the date. The date is written following the ISO 8601 standard.
import org.json4s._ import org.json4s.Extraction.decompose import org.json4s.jackson.JsonMethods.{parse, compact} import java.util.Date implicit val formats = DefaultFormats val txt = compact(decompose(Map("date" -> new Date()))) // txt: String = {"date":"2014-11-19T19:34:34Z"} val date = (parse(txt) "date").extractOpt[Date] // date: Option[java.util.Date] = Some(Wed Nov 19 20:34:34 CET 2014)
A custom Formats is most easily created by extending the DefaultFormats trait. As an exercise, you can create one that employs a custom date format, MM/dd/YYYY.
import java.text.SimpleDateFormat implicit val formats = new DefaultFormats { override protected def dateFormatter: SimpleDateFormat = { new SimpleDateFormat("MM/dd/yyyy") } } val txt = compact(decompose(Map("date" -> new Date()))) // txt: String = {"date":"11/19/2014"} val date = (parse(txt) "date").extractOpt[Date] // date: Option[java.util.Date] = Some(Wed Nov 19 00:00:00 CET 2014)
A field serializer is useful when those fields that don’t appear in a class constructor should be serialized. Assume you have a class, Foo, that you want to serialize with all its fields. The constructor has two parameters: x and y. Because y is prefixed by a val, there’s also a y field. Additionally, there are a and b fields:
class Foo(x: String, val y: String) { private val a: String = "a" var b: String = "b" }
With the default serialization, only the single field, y, that appears in the constructor is written to the JSON during serialization:
import org.json4s._ import org.json4s.JsonDSL._ import org.json4s.jackson.Serialization.write implicit val formats = DefaultFormats val foo = new Foo("x", "y") val txt1 = write(foo) // txt1: String = {"y":"y"}
In this example, you want all fields, y, a, and b, to appear. A field serializer does exactly that.
You can add a FieldSerializer[Food] using the + operator, resulting in a new Formats. Now all fields are taken into account:
import org.json4s._ import org.json4s.JsonDSL._ import org.json4s.jackson.Serialization implicit val formats = DefaultFormats + new FieldSerializer[Foo]() val foo = new Foo("x", "y") val txt1 = Serialization.write(foo) // txt1: String = {"y":"y","a":"a","b":"b"}
Section 5.3.3 shows how to further specialize the JSON handling for a type featuring custom serializers. Let’s next look at how to employ type hints.
In this section, we’ll discuss how to use type hints, which are a way to work with polymorphic values. Let’s assume that you need to work with the class hierarchy shown next. The Measure data type describes common measures that may appear in a recipe, and it improves the expressivity of the recipe model:
sealed trait Measure case class Gram(value: Double) extends Measure case class Teaspoon(value: Double) extends Measure case class Tablespoon(value: Double) extends Measure case class Handful(value: Double) extends Measure case class Pieces(value: Double) extends Measure case class Milliliter(value: Double) extends Measure
Let’s further assume that you want to create a JSON array from a list of such values and read it back. This is the JSON that’s generated by default:
val amounts = List(Handful(2), Gram(300), Teaspoon(2)) val amountsJson = Extraction.decompose(amounts) [ { "value" : 2 }, { "value" : 300 }, { "value" : 2 } ]
Each element consists of a single field, v.
Note that by looking at the JSON representation, there’s no clear way to determine what subtype of Amount a single element of this array represents. Consequently, it’s not possible to read this array back to a List[Amount]. Instead, this results in the following exception:
amountsJson.extract[List[Measure]] // MappingException: // No constructor for type Measure, JObject(List((v,JInt(2))))
One way out of this dilemma is to use a synthetic field that holds type information. In Json4s, such a field is called a type hint. By default, the key is jsonClass and the value is equal to the name of the respective type. When a value is decomposed to a JSON object, a type hint field is added to it automatically, and when a value is extracted from a JSON object, the type hint is used to infer the actual type. In order to enable type hints, you can use the withHints method and provide a type hint configuration, as follows.
import org.json4s.Extraction.decompose import org.json4s.jackson.JsonMethods.pretty val hints = ShortTypeHints(List( classOf[Gram], classOf[Tablespoon], classOf[Teaspoon], classOf[Handful])) implicit val formats = DefaultFormats.withHints(hints) val amountsJson = decompose(amounts) pretty(amountsJson) // [ { // "jsonClass" : "Handful", // "v" : 2 // }, ... // ] amountsJson.extract[List[Measure]] // res: List[Amount] = List(Handful(2), Gram(300), Teaspoon(2))
Here you use ShortTypeHints with a list of all classes where type hints should be enabled. The resulting JSON correctly interprets the objects, because there are no long ambiguities.
The format of the type hint field can also be customized. For example, there could already be a different naming convention for a type hint field other than jsonClass, or the class name could follow a special format. You can customize these two things.
Let’s say you need to use the key _type. In order to define a non-default field name, the value typeHintFieldName can be overridden:
implicit val jsonFormats = new DefaultFormats { override val typeHintFieldName: String = "_type" }
For the type hint value, there’s an alternative implementation in the form of FullTypeHints that uses the full class name.
The type hints can be overridden in the Formats as well:
implicit val jsonFormats = DefaultFormats.withHints(FullTypeHints(List( classOf[Gram], classOf[Tablespoon], classOf[Teaspoon], classOf[Handful])))
Type hints are a simple but effective approach to working with class hierarchies in polymorphic collections and fields. Let’s now move on and look at how to work with custom serializers.
A custom serializer defines the JSON conversion for a specific type. In the case of a syntactical mismatch between JSON and a Scala type, a custom serializer can help you align the two formats through syntactical and structural transformations. In this section, we’ll look at how you can make use of custom serializers.
Let’s say your application wants to retrieve and send nutritional facts about food products, and the facts are represented as values of type NutritionFacts. Instead of typing the facts as primitives, they’re represented as subtypes of Fact. This improves the expressivity of the model and prevents errors by early and strongly typing. The model is shown in the following listing.
sealed trait Fact case class Energy(value: Int) extends Fact case class Carbohydrate(value: Double) extends Fact case class Fat(value: Double) extends Fact case class Protein(value: Double) extends Fact case class NutritionFacts( energy: Energy, carbohydrate: Carbohydrate, fat: Fat, protein: Protein)
The nutritional facts should be sent and received as JSON. The expected JSON format for the public API is defined as follows:
{ "energy": 2050 "fat": 33.9, "carbohydrate": 36.2, "protein": 7.9 }
Your goal is to be able to read and write a NutritionFacts value in exactly that format. Serializing a value to JSON using the default conversion yields the following:
val facts = NutritionFacts( Energy(2050), Carbohydrate(36.2), Fat(33.9), Protein(7.9)) pretty(decompose(facts)) // { // "energy": { // "value": 2050 // }, // ... // }
This isn’t exactly what you want. Each fact should be written as a JSON number and not as an object. Trying to read your expected JSON document also results in an extraction error:
val jsObj = parse(""" { "energy": 2050, "carbohydrate": 36.2, "fat": 33.9, "protein": 7.9 } """) jsObj.extractOpt[NutritionFacts] // None
In order to fix the mismatch between the two formats, you could adjust either your domain model or your public API. For example, you could primitively type the fact’s values as Double instead.
But obviously this isn’t always possible or preferable. Take as an example a big project employing a legacy data model or legacy API. Furthermore, adjusting a model just to overcome a technical problem is rarely an ideal solution.
What can you do instead? You can write a custom serializer. A custom serializer basically represents two partial functions: one function accepts a JValue and returns a NutritionFacts; the other function knows how to create a NutritionFacts from a JSON document.
The following listing shows how to define a custom serializer for the example. Note that you can use the full power of operations discussed in section 5.2.
class NutritionFactsSerializer extends CustomSerializer[NutritionFacts](implicit formats => ({ case jv: JValue => val e = (jv "energy").extract[Int] val c = (jv "carbohydrate").extract[Double] val f = (jv "fat").extract[Double] val p = (jv "protein").extract[Double] NutritionFacts( Energy(e), Carbohydrate(c), Fat(f), Protein(p)) }, { case facts: NutritionFacts => ("energy" -> facts.energy.value) ~ ("carbohydrate" -> facts.carbohydrate.value) ~ ("fat" -> facts.fat.value) ~ ("protein" -> facts.protein.value) }))
The custom serializer needs to be registered in the implicit Formats value. This can be achieved by using the + operator, which takes a CustomSerializer as an argument and returns a new Formats. The following listing shows an example with two routes using the custom serializer.
trait FoodRoutes extends ScalatraBase with JacksonJsonSupport { implicit val jsonFormats = DefaultFormats + new NutritionFactsSerializer get("/foods/foo_bar/facts") { val facts = NutritionFacts( Energy(2050), Carbohydrate(36.2), Fat(33.9), Protein(7.9)) val factsJson = Extraction.decompose(facts) factsJson } post("/foods/:name/facts") { val facts = parsedBody.extractOpt[NutritionFacts] println(f"updated facts: $facts") } }
This should give you enough power to handle even major format mismatches.
If you’ve already developed a browser-based web 2.0 application, you’ve probably heard of the same origin policy (SOP). This is a security measure that should prevent a resource from accessing resources located on a different host than the resource itself. It therefore prevents cross-site requests.
But sometimes you’ll want your website to do exactly that, such as querying a web service located at another host using an Ajax request. With the SOP, this isn’t possible without further ado. JSONP (JSON with Padding) allows you to work around SOP. It issues an HTTP request by inserting a <script /> tag into the DOM instead of using Ajax. This works because the src attribute of a script isn’t subject to SOP.
A JSONP request can be detected by the JSON module, and in the case of a JSONP request, the web service returns JavaScript source code instead of JSON. That JavaScript represents a function call passing the JSON data as argument. The name of the function is given as a query parameter.
A JSONP request is detected by comparing the request parameters against a predefined set of callback parameter names. Those parameters can be set by overriding the jsonpCallbackParameterNames: Iterable[String] method, as follows.
trait MyJsonpRoutes extends ScalatraBase with JacksonJsonSupport { override def jsonpCallbackParameterNames = Seq("jsonp") implicit val jsonFormats = DefaultFormats get("/foods/foo_bar") { val productJson = ("label" -> "Foo bar") ~ ("fairTrade" -> true) ~ ("tags" -> List("bio", "chocolate")) productJson } }
The route can now be called with a query parameter, jsonp, that specifies the name of the local JavaScript method that should handle the response. The resulting JSON is returned as a JSONP response. Note that the content type of the response is text/javascript:
curl -v http://localhost:8080/foods/foo_bar?jsonp=handleResponse > GET /foods/foo_bar?jsonp=handleResponse HTTP/1.1 > User-Agent: curl/7.37.1 > Host: localhost:8080 > Accept: */* > < HTTP/1.1 200 OK < Date: Wed, 01 Jul 2015 12:05:00 GMT < Content-Type: text/javascript; charset=UTF-8 < Content-Length: 84 < /**/handleResponse({"label":"Foo bar","fairTrade":true,"tags":["bio","chocolate"]});
3.22.41.235