Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alternative product formats #93

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
Version 1.3.0 (2014-09-22)
--------------------------
- Upgraded to Scala 2.11.2, dropped support for Scala 2.9
- Switched to fast, hand-written parser (#86, #108)
- Removed dependency on parboiled
- Changed parser to produce JsObject(HashMap) rather than JsObject(ListMap)
- Switched JsArray(List) to JsArray(Vector)
- Improved JsonPrinter to support printing to custom StringBuilder
- Added support for parameter-less case classes (#41)


Version 1.2.6 (2014-04-10)
--------------------------
- Improved deserialization error message with name of malformed field (#62)
Expand Down
38 changes: 26 additions & 12 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ _spray-json_ is a lightweight, clean and efficient [JSON] implementation in Scal
It sports the following features:

* A simple immutable model of the JSON language elements
* An efficient JSON PEG parser (implemented with [parboiled][])
* An efficient JSON parser
* Choice of either compact or pretty JSON-to-string printing
* Type-class based (de)serialization of custom objects (no reflection, no intrusion)
* No external dependencies

_spray-json_ allows you to convert between
* String JSON documents
Expand All @@ -19,28 +20,22 @@ as depicted in this diagram:
### Installation

_spray-json_ is available from the [repo.spray.io] repository.
The latest release is `1.2.6` and is built against Scala 2.9.3, Scala 2.10.4 and Scala 2.11.0-RC4.
The latest release is `1.3.0` and is built against Scala 2.10.4 and Scala 2.11.2.
Copy link
Member

@jrudolph jrudolph Oct 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove patch versions as they shouldn't matter (i.e. "2.10.x" and "2.11.x" and "2.12.x" and "2.13.0-M2"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed that in #239


If you use SBT you can include _spray-json_ in your project with

```scala
resolvers += "spray" at "http://repo.spray.io/"

libraryDependencies += "io.spray" %% "spray-json" % "1.2.6"
libraryDependencies += "io.spray" %% "spray-json" % "1.3.0"
```

_spray-json_ has only one dependency: the parsing library [parboiled][]
(which is also a dependency of _spray-http_, so if you use _spray-json_ together with other modules of the *spray*
suite you are not incurring any additional dependency).

### Usage

_spray-json_ is really easy to use.
Just bring all relevant elements in scope with

```scala
import spray.json._
import DefaultJsonProtocol._ // !!! IMPORTANT, else `convertTo` and `toJson` won't work correctly
import DefaultJsonProtocol._ // if you don't supply your own Protocol (see below)
```

and do one or more of the following:
Expand Down Expand Up @@ -92,7 +87,7 @@ protocol need to be "mece" (mutually exclusive, collectively exhaustive), i.e. t
together need to span all types required by the application.

This may sound more complicated than it is.
_spray-json_ comes with a `DefaultJsonProtocol`, which already covers all of Scalas value types as well as the most
_spray-json_ comes with a `DefaultJsonProtocol`, which already covers all of Scala's value types as well as the most
important reference and collection types. As long as your code uses nothing more than these you only need the
`DefaultJsonProtocol`. Here are the types already taken care of by the `DefaultJsonProtocol`:

Expand Down Expand Up @@ -158,6 +153,26 @@ object MyJsonProtocol extends DefaultJsonProtocol {
}
```

### Alternative format for Case Classes

As of version 1.2.6, there is an alternative method to create formats for case classes. Instead of `jsonFormatN` use
`formatN`. Apart from this, the new formats are a drop-in replacement for the original ones.

```scala
case class Color(name: String, red: Int, green: Int, blue: Int)

object MyJsonProtocol extends DefaultJsonProtocol {
implicit val colorFormat = format4(Color)
}
```

The new method has several advantages:

* Use default values defined on case-classes when fields are missing from json
* Omit empty arrays and objects when serializing fields with default values
* Allows property renaming
* Override formats per field
* Allow advanced customizations

#### NullOptions

Expand Down Expand Up @@ -289,7 +304,6 @@ _spray-json_ project under the project’s open source license.


[JSON]: http://json.org
[parboiled]: http://parboiled.org
[repo.spray.io]: http://repo.spray.io
[SJSON]: https://github.com/debasishg/sjson
[Databinder-Dispatch]: https://github.com/n8han/Databinder-Dispatch
Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name := "spray-json"

version := "1.3.0-SNAPSHOT"
version := "1.3.0"

organization := "io.spray"

Expand Down
35 changes: 17 additions & 18 deletions src/main/scala/spray/json/CollectionFormats.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ trait CollectionFormats {
* Supplies the JsonFormat for Lists.
*/
implicit def listFormat[T :JsonFormat] = new RootJsonFormat[List[T]] {
def write(list: List[T]) = JsArray(list.map(_.toJson))
def read(value: JsValue) = value match {
case JsArray(elements) => elements.map(_.convertTo[T])
def write(list: List[T]) = JsArray(list.map(_.toJson).toVector)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these changes in here need to be performance verified. It would be easier if they'd be broken out into an extra PR.

def read(value: JsValue): List[T] = value match {
case JsArray(elements) => elements.map(_.convertTo[T])(collection.breakOut)
case x => deserializationError("Expected List as JsArray, but got " + x)
}
}
Expand All @@ -34,7 +34,7 @@ trait CollectionFormats {
* Supplies the JsonFormat for Arrays.
*/
implicit def arrayFormat[T :JsonFormat :ClassManifest] = new RootJsonFormat[Array[T]] {
def write(array: Array[T]) = JsArray(array.map(_.toJson).toList)
def write(array: Array[T]) = JsArray(array.map(_.toJson).toVector)
def read(value: JsValue) = value match {
case JsArray(elements) => elements.map(_.convertTo[T]).toArray[T]
case x => deserializationError("Expected Array as JsArray, but got " + x)
Expand Down Expand Up @@ -64,31 +64,30 @@ trait CollectionFormats {

import collection.{immutable => imm}

implicit def immIterableFormat[T :JsonFormat] = viaList[imm.Iterable[T], T](list => imm.Iterable(list :_*))
implicit def immSeqFormat[T :JsonFormat] = viaList[imm.Seq[T], T](list => imm.Seq(list :_*))
implicit def immIndexedSeqFormat[T :JsonFormat] = viaList[imm.IndexedSeq[T], T](list => imm.IndexedSeq(list :_*))
implicit def immLinearSeqFormat[T :JsonFormat] = viaList[imm.LinearSeq[T], T](list => imm.LinearSeq(list :_*))
implicit def immSetFormat[T :JsonFormat] = viaList[imm.Set[T], T](list => imm.Set(list :_*))
implicit def vectorFormat[T :JsonFormat] = viaList[Vector[T], T](list => Vector(list :_*))
implicit def immIterableFormat[T :JsonFormat] = viaSeq[imm.Iterable[T], T](seq => imm.Iterable(seq :_*))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add explicit return types to each of these (check with mima that they don't change).

implicit def immSeqFormat[T :JsonFormat] = viaSeq[imm.Seq[T], T](seq => imm.Seq(seq :_*))
implicit def immIndexedSeqFormat[T :JsonFormat] = viaSeq[imm.IndexedSeq[T], T](seq => imm.IndexedSeq(seq :_*))
implicit def immLinearSeqFormat[T :JsonFormat] = viaSeq[imm.LinearSeq[T], T](seq => imm.LinearSeq(seq :_*))
implicit def immSetFormat[T :JsonFormat] = viaSeq[imm.Set[T], T](seq => imm.Set(seq :_*))
implicit def vectorFormat[T :JsonFormat] = viaSeq[Vector[T], T](seq => Vector(seq :_*))

import collection._

implicit def iterableFormat[T :JsonFormat] = viaList[Iterable[T], T](list => Iterable(list :_*))
implicit def seqFormat[T :JsonFormat] = viaList[Seq[T], T](list => Seq(list :_*))
implicit def indexedSeqFormat[T :JsonFormat] = viaList[IndexedSeq[T], T](list => IndexedSeq(list :_*))
implicit def linearSeqFormat[T :JsonFormat] = viaList[LinearSeq[T], T](list => LinearSeq(list :_*))
implicit def setFormat[T :JsonFormat] = viaList[Set[T], T](list => Set(list :_*))
implicit def iterableFormat[T :JsonFormat] = viaSeq[Iterable[T], T](seq => Iterable(seq :_*))
implicit def seqFormat[T :JsonFormat] = viaSeq[Seq[T], T](seq => Seq(seq :_*))
implicit def indexedSeqFormat[T :JsonFormat] = viaSeq[IndexedSeq[T], T](seq => IndexedSeq(seq :_*))
implicit def linearSeqFormat[T :JsonFormat] = viaSeq[LinearSeq[T], T](seq => LinearSeq(seq :_*))
implicit def setFormat[T :JsonFormat] = viaSeq[Set[T], T](seq => Set(seq :_*))

/**
* A JsonFormat construction helper that creates a JsonFormat for an Iterable type I from a builder function
* List => I.
*/
def viaList[I <: Iterable[T], T :JsonFormat](f: List[T] => I): RootJsonFormat[I] = new RootJsonFormat[I] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a public method so it needs a proper deprecation cycle.

def write(iterable: I) = JsArray(iterable.map(_.toJson).toList)
def viaSeq[I <: Iterable[T], T :JsonFormat](f: imm.Seq[T] => I): RootJsonFormat[I] = new RootJsonFormat[I] {
def write(iterable: I) = JsArray(iterable.map(_.toJson).toVector)
def read(value: JsValue) = value match {
case JsArray(elements) => f(elements.map(_.convertTo[T]))
case x => deserializationError("Expected Collection as JsArray, but got " + x)
}
}

}
2 changes: 1 addition & 1 deletion src/main/scala/spray/json/CompactPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ trait CompactPrinter extends JsonPrinter {
sb.append('}')
}

protected def printArray(elements: List[JsValue], sb: StringBuilder) {
protected def printArray(elements: Seq[JsValue], sb: StringBuilder) {
sb.append('[')
printSeq(elements, sb.append(','))(print(_, sb))
sb.append(']')
Expand Down
12 changes: 5 additions & 7 deletions src/main/scala/spray/json/JsValue.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

package spray.json

import collection.immutable.ListMap
import collection.immutable

/**
* The general type of a JSON AST node.
Expand Down Expand Up @@ -49,20 +49,18 @@ sealed abstract class JsValue {
*/
case class JsObject(fields: Map[String, JsValue]) extends JsValue {
override def asJsObject(errorMsg: String) = this
def getFields(fieldNames: String*): Seq[JsValue] = fieldNames.flatMap(fields.get)
def getFields(fieldNames: String*): immutable.Seq[JsValue] = fieldNames.flatMap(fields.get)(collection.breakOut)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be a binary compatibility problem? If yes, needs deprecated forwarder.

}
object JsObject {
// we use a ListMap in order to preserve the field order
def apply(members: JsField*) = new JsObject(ListMap(members: _*))
def apply(members: List[JsField]) = new JsObject(ListMap(members: _*))
def apply(members: JsField*) = new JsObject(Map(members: _*))
}

/**
* A JSON array.
*/
case class JsArray(elements: List[JsValue]) extends JsValue
case class JsArray(elements: Vector[JsValue]) extends JsValue
object JsArray {
def apply(elements: JsValue*) = new JsArray(elements.toList)
def apply(elements: JsValue*) = new JsArray(elements.toVector)
}

/**
Expand Down
15 changes: 7 additions & 8 deletions src/main/scala/spray/json/JsonParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import java.lang.{StringBuilder => JStringBuilder}
import java.nio.{CharBuffer, ByteBuffer}
import java.nio.charset.Charset
import scala.annotation.{switch, tailrec}
import scala.collection.immutable.ListMap

/**
* Fast, no-dependency parser for JSON as defined by http://tools.ietf.org/html/rfc4627.
Expand All @@ -29,7 +28,7 @@ object JsonParser {
def apply(input: ParserInput): JsValue = new JsonParser(input).parseJsValue()

class ParsingException(val summary: String, val detail: String = "")
extends RuntimeException(if (summary.isEmpty) detail else if (detail.isEmpty) summary else summary + ": " + detail)
extends RuntimeException(if (summary.isEmpty) detail else if (detail.isEmpty) summary else summary + ":" + detail)
}

class JsonParser(input: ParserInput) {
Expand Down Expand Up @@ -72,7 +71,7 @@ class JsonParser(input: ParserInput) {
// http://tools.ietf.org/html/rfc4627#section-2.2
private def `object`(): Unit = {
ws()
var map = ListMap.empty[String, JsValue]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will probably lead to incompatible semantic change.

var map = Map.empty[String, JsValue]
@tailrec def members(): Unit = {
`string`()
require(':')
Expand All @@ -91,7 +90,7 @@ class JsonParser(input: ParserInput) {
// http://tools.ietf.org/html/rfc4627#section-2.3
private def `array`(): Unit = {
ws()
var list = List.newBuilder[JsValue]
var list = Vector.newBuilder[JsValue]
@tailrec def values(): Unit = {
`value`()
list += jsValue
Expand Down Expand Up @@ -192,7 +191,7 @@ class JsonParser(input: ParserInput) {
}
val detail = {
val sanitizedText = text.map(c ⇒ if (Character.isISOControl(c)) '?' else c)
s"\n$sanitizedText\n${" " * col}^\n"
s"\n$sanitizedText\n${" " * (col-1)}^\n"
}
throw new ParsingException(summary, detail)
}
Expand Down Expand Up @@ -236,11 +235,11 @@ object ParserInput {
@tailrec def rec(ix: Int, lineStartIx: Int, lineNr: Int): Line =
nextUtf8Char() match {
case '\n' if index > ix => sb.setLength(0); rec(ix + 1, ix + 1, lineNr + 1)
case '\n' | EOI => Line(lineNr, index - lineStartIx, sb.toString)
case '\n' | EOI => Line(lineNr, index - lineStartIx + 1, sb.toString)
case c => sb.append(c); rec(ix + 1, lineStartIx, lineNr)
}
val savedCursor = _cursor
_cursor = 0
_cursor = -1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that a bug fix?

val line = rec(ix = 0, lineStartIx = 0, lineNr = 1)
_cursor = savedCursor
line
Expand All @@ -250,7 +249,7 @@ object ParserInput {
private val UTF8 = Charset.forName("UTF-8")

/**
* ParserInput reading directly off a byte array which is assumed to contain the UTF-8 encoded represenation
* ParserInput reading directly off a byte array which is assumed to contain the UTF-8 encoded representation
* of the JSON input, without requiring a separate decoding step.
*/
class ByteArrayBasedParserInput(bytes: Array[Byte]) extends DefaultParserInput {
Expand Down
20 changes: 9 additions & 11 deletions src/main/scala/spray/json/JsonPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package spray.json

import annotation.tailrec
import java.lang.StringBuilder
import java.lang.{StringBuilder => JStringBuilder}

/**
* A JsonPrinter serializes a JSON AST to a String.
Expand All @@ -26,24 +26,22 @@ trait JsonPrinter extends (JsValue => String) {

def apply(x: JsValue): String = apply(x, None)

def apply(x: JsValue, jsonpCallback: String): String = apply(x, Some(jsonpCallback))

def apply(x: JsValue, jsonpCallback: Option[String]): String = {
val sb = new StringBuilder
def apply(x: JsValue,
jsonpCallback: Option[String] = None,
sb: JStringBuilder = new JStringBuilder(256)): String = {
jsonpCallback match {
case Some(callback) => {
case Some(callback) =>
sb.append(callback).append('(')
print(x, sb)
sb.append(')');
}
sb.append(')')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

case None => print(x, sb)
}
sb.toString
}

def print(x: JsValue, sb: StringBuilder)
def print(x: JsValue, sb: JStringBuilder)

protected def printLeaf(x: JsValue, sb: StringBuilder) {
protected def printLeaf(x: JsValue, sb: JStringBuilder) {
x match {
case JsNull => sb.append("null")
case JsTrue => sb.append("true")
Expand All @@ -54,7 +52,7 @@ trait JsonPrinter extends (JsValue => String) {
}
}

protected def printString(s: String, sb: StringBuilder) {
protected def printString(s: String, sb: JStringBuilder) {
import JsonPrinter._
@tailrec def firstToBeEncoded(ix: Int = 0): Int =
if (ix == s.length) -1 else if (requiresEncoding(s.charAt(ix))) ix else firstToBeEncoded(ix + 1)
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/spray/json/PrettyPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ trait PrettyPrinter extends JsonPrinter {
sb.append("}")
}

protected def printArray(elements: List[JsValue], sb: StringBuilder, indent: Int) {
protected def printArray(elements: Seq[JsValue], sb: StringBuilder, indent: Int) {
sb.append('[')
printSeq(elements, sb.append(", "))(print(_, sb, indent))
sb.append(']')
Expand Down
Loading