Skip to content

Commit

Permalink
Removing support for the undefined keyword, which is part of JavaSc…
Browse files Browse the repository at this point in the history
…ript but not JSON itself.
  • Loading branch information
bathalh committed Nov 1, 2017
1 parent 90908f4 commit 60f2687
Show file tree
Hide file tree
Showing 12 changed files with 85 additions and 44 deletions.
17 changes: 8 additions & 9 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,14 @@ For example, a PATCH request may have a payload like this:
```json
{ "id":"234565434567898789098765",
"field1": "new value",
"field3": null,
"field4": undefined }
"field3": null }
```
which would tell the server to update field1 to "new value", set field3 to null, and leave field2 and field4
unchanged. With a standard scala `Option`, it is impossible to tell whether the values of field2, field3,
and field4 in the original payload were `null` or `undefined` since any missing values translate to `None`.
which would tell the server to update field1 to "new value", set field3 to null, and leave field2
unchanged. With a standard scala `Option`, it is impossible to tell whether the values of field2 and field3
in the original payload were `null` or undefined since any missing values translate to `None`.

The `Tription` solves that problem by defining `Value` for present values, `Null` for null values, and
`Undefined` for values which are missing or explicitly marked as undefined.
The `Tription` solves that problem by defining `Value` for present values, `Null` for values explicitly marked
null, and `Undefined` for values which are missing.

`Tription`s can be used just like `Option`s:
```scala
Expand Down Expand Up @@ -184,8 +183,8 @@ object MyJsonProtocol extends DefaultJsonProtocol {
#### NullOptions

The `NullOptions` trait supplies an alternative rendering mode for optional case class members. Normally optional
members that are undefined (`None`/`Undefined`) are not rendered at all. By mixing in this trait into your custom JsonProtocol you
can enforce the rendering of undefined members as `null`.
members that are undefined (`None`/`Undefined`) are not rendered at all. By mixing in this trait into your custom
JsonProtocol you can enforce the rendering of undefined members as `null`.
(Note that this only affect JSON writing, spray-json will always read missing `Option` members as well as `null`
`Option` members as `None` and missing `Tription` members as `Undefined`.)

Expand Down
9 changes: 5 additions & 4 deletions src/main/scala/spray/json/CompactPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ trait CompactPrinter extends JsonPrinter {

protected def printObject(members: Map[String, JsValue], sb: StringBuilder) {
sb.append('{')
printSeq(members, sb.append(',')) { m =>
printString(m._1, sb)
sb.append(':')
print(m._2, sb)
val definedMembers = members filter { case (_, v) => v != JsUndefined }
printSeq(definedMembers, sb.append(',')) { m =>
printString( m._1, sb )
sb.append( ':' )
print( m._2, sb )
}
sb.append('}')
}
Expand Down
5 changes: 3 additions & 2 deletions src/main/scala/spray/json/JsValue.scala
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ case class JsArray(elements: Vector[JsValue]) extends JsValue {
}
object JsArray {
val empty = JsArray(Vector.empty)
def apply(elements: JsValue*) = new JsArray(elements.toVector)
def apply(elements: JsValue*) = if( elements contains JsUndefined ) throw new IllegalStateException( "JSON arrays cannot contain undefined values" )
else new JsArray(elements.toVector)
@deprecated("Use JsArray(Vector[JsValue]) instead", "1.3.0")
def apply(elements: List[JsValue]) = new JsArray(elements.toVector)
}
Expand Down Expand Up @@ -123,5 +124,5 @@ case object JsFalse extends JsBoolean {
*/
case object JsNull extends JsValue

/** The representation for JSON undefined. **/
/** The representation for JSON missing value. **/
case object JsUndefined extends JsValue
2 changes: 0 additions & 2 deletions src/main/scala/spray/json/JsonParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ class JsonParser(input: ParserInput) {
(cursorChar: @switch) match {
case 'f' => simpleValue(`false`(), JsFalse)
case 'n' => simpleValue(`null`(), JsNull)
case 'u' => simpleValue(`undefined`(), JsUndefined)
case 't' => simpleValue(`true`(), JsTrue)
case '{' => advance(); `object`()
case '[' => advance(); `array`()
Expand All @@ -72,7 +71,6 @@ class JsonParser(input: ParserInput) {

private def `false`() = advance() && ch('a') && ch('l') && ch('s') && ws('e')
private def `null`() = advance() && ch('u') && ch('l') && ws('l')
private def `undefined`() = advance() && ch('n') && ch('d') && ch('e') && ch('f') && ch('i') && ch('n') && ch('e') && ws('d')
private def `true`() = advance() && ch('r') && ch('u') && ws('e')

// http://tools.ietf.org/html/rfc4627#section-2.2
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/spray/json/JsonPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ trait JsonPrinter extends (JsValue => String) {
protected def printLeaf(x: JsValue, sb: JStringBuilder) {
x match {
case JsNull => sb.append("null")
case JsUndefined => sb.append("undefined")
case JsTrue => sb.append("true")
case JsFalse => sb.append("false")
case JsNumber(x) => sb.append(x)
case JsString(x) => printString(x, sb)
case JsUndefined => throw new IllegalStateException( "Cannot display JsUndefined" )
case _ => throw new IllegalStateException
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/main/scala/spray/json/PrettyPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ trait PrettyPrinter extends JsonPrinter {
protected def organiseMembers(members: Map[String, JsValue]): Seq[(String, JsValue)] = members.toSeq

protected def printObject(members: Map[String, JsValue], sb: StringBuilder, indent: Int) {
sb.append("{\n")
printSeq(organiseMembers(members), sb.append(",\n")) { m =>
sb.append("{\n")
val definedMembers = members filter { case (_, v) => v != JsUndefined }
printSeq(organiseMembers(definedMembers), sb.append(",\n")) { m =>
printIndent(sb, indent + Indent)
printString(m._1, sb)
sb.append(": ")
Expand Down
6 changes: 3 additions & 3 deletions src/main/scala/spray/json/ProductFormats.scala
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,17 @@ trait ProductFormats extends ProductFormatsInstances {
protected def fromField[T](value: JsValue, fieldName: String)
(implicit reader: JsonReader[T]) = value match {
case x: JsObject if
(reader.isInstanceOf[OptionFormat[_]] &
(reader.isInstanceOf[OptionFormat[_]] &
!x.fields.contains(fieldName)) =>
None.asInstanceOf[T]
case x: JsObject if
(reader.isInstanceOf[TriptionFormat[_]] &
(reader.isInstanceOf[TriptionFormat[_]] &
!x.fields.contains(fieldName)) =>
Undefined.asInstanceOf[T]
case x: JsObject =>
try reader.read(x.fields(fieldName))
catch {
case e: NoSuchElementException => Undefined
case e: NoSuchElementException =>
deserializationError("Object is missing required member '" + fieldName + "'", e, fieldName :: Nil)
case DeserializationException(msg, cause, fieldNames) =>
deserializationError(msg, cause, fieldName :: fieldNames)
Expand Down
6 changes: 3 additions & 3 deletions src/main/scala/spray/json/Tription.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ package spray.json
* "field3: null }
* </code>
* which would tell the server to update field1 to 7, set field3 to null, and leave field2 alone.
* With a standard scala `Option`, it is impossible to tell whether the payload of the request had field2 and field3
* With a standard scala `Option`, it is impossible to tell whether field2 and field3 were
* null or undefined since any missing values translate to `None`.
*
* The Tription solves that problem by defining `Value` for present values, `Null` for null values, and
* `Undefined` for values which are missing or explicitly marked as undefined.
* The Tription solves that problem by defining `Value` for present values, `Null` for values
* explicitly set to null, and `Undefined` for values which are not there at all.
*
* Created by bathalh on 2/19/16.
*/
Expand Down
21 changes: 17 additions & 4 deletions src/test/scala/spray/json/CompactPrinterSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,13 @@ class CompactPrinterSpec extends Specification {
"print JsNull to 'null'" in {
CompactPrinter(JsNull) mustEqual "null"
}
"print JsUndefined to 'undefined'" in {
CompactPrinter(JsUndefined) mustEqual "undefined"
"throw exception when printing JsUndefined" in {
try {
CompactPrinter(JsUndefined) mustEqual "undefined"
} catch {
case ise: IllegalStateException =>
ise.getMessage mustEqual "Cannot display JsUndefined"
}
}
"print JsTrue to 'true'" in {
CompactPrinter(JsTrue) mustEqual "true"
Expand Down Expand Up @@ -67,9 +72,17 @@ class CompactPrinterSpec extends Specification {
CompactPrinter(JsObject("key" -> JsNumber(42), "key2" -> JsString("value")))
mustEqual """{"key":42,"key2":"value"}"""
)
"properly print a simple JsObject with undefined values" in (
CompactPrinter(JsObject("key" -> JsNumber(42), "key2" -> JsString("value"), "key3" -> JsUndefined))
mustEqual """{"key":42,"key2":"value"}"""
)
"properly print a simple JsObject with only undefined values" in (
CompactPrinter(JsObject("key" -> JsUndefined, "key2" -> JsUndefined))
mustEqual "{}"
)
"properly print a simple JsArray" in (
CompactPrinter(JsArray(JsNull, JsUndefined, JsNumber(1.23), JsObject("key" -> JsBoolean(true))))
mustEqual """[null,undefined,1.23,{"key":true}]"""
CompactPrinter(JsArray(JsNull, JsNumber(1.23), JsObject("key" -> JsBoolean(true))))
mustEqual """[null,1.23,{"key":true}]"""
)
"properly print a JSON padding (JSONP) if requested" in {
CompactPrinter(JsTrue, Some("customCallback")) mustEqual("customCallback(true)")
Expand Down
7 changes: 2 additions & 5 deletions src/test/scala/spray/json/JsonParserSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ class JsonParserSpec extends Specification {
"parse 'null' to JsNull" in {
JsonParser("null") === JsNull
}
"parse 'undefined' to JsUndefined" in {
JsonParser("undefined") === JsUndefined
}
"parse 'true' to JsTrue" in {
JsonParser("true") === JsTrue
}
Expand Down Expand Up @@ -60,8 +57,8 @@ class JsonParserSpec extends Specification {
JsObject("key" -> JsNumber(42), "key2" -> JsString("value"))
)
"parse a simple JsArray" in (
JsonParser("""[null, undefined, 1.23 ,{"key":true } ] """) ===
JsArray(JsNull, JsUndefined, JsNumber(1.23), JsObject("key" -> JsTrue))
JsonParser("""[null, 1.23 ,{"key":true } ] """) ===
JsArray(JsNull, JsNumber(1.23), JsObject("key" -> JsTrue))
)
"parse directly from UTF-8 encoded bytes" in {
val json = JsObject(
Expand Down
43 changes: 34 additions & 9 deletions src/test/scala/spray/json/PrettyPrinterSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,50 @@ import org.specs2.mutable._
class PrettyPrinterSpec extends Specification {

"The PrettyPrinter" should {
val JsObject(fields) = JsonParser {
"""{
| "Boolean no": false,
| "Boolean yes":true,
| "Unic\u00f8de" : "Long string with newline\nescape",
| "key with \"quotes\"" : "string",
| "key with spaces": null,
| "number": -1.2323424E-5,
| "simpleKey" : "some value",
| "sub object" : {
| "sub key": 26.5,
| "a": "b",
| "array": [1, 2, { "yes":1, "no":0 }, ["a", "b", null], false]
| },
| "zero": 0
|}""".stripMargin
}

"print a more complicated JsObject nicely aligned" in {
val JsObject(fields) = JsonParser {
PrettyPrinter(JsObject(ListMap(fields.toSeq.sortBy(_._1):_*))) mustEqual {
"""{
| "Boolean no": false,
| "Boolean yes":true,
| "Unic\u00f8de" : "Long string with newline\nescape",
| "key with \"quotes\"" : "string",
| "Boolean yes": true,
| "Unic\u00f8de": "Long string with newline\nescape",
| "key with \"quotes\"": "string",
| "key with spaces": null,
| "number": -1.2323424E-5,
| "simpleKey" : "some value",
| "sub object" : {
| "number": -0.000012323424,
| "simpleKey": "some value",
| "sub object": {
| "sub key": 26.5,
| "a": "b",
| "array": [1, 2, { "yes":1, "no":0 }, ["a", "b", null], false]
| "array": [1, 2, {
| "yes": 1,
| "no": 0
| }, ["a", "b", null], false]
| },
| "zero": 0
|}""".stripMargin
}
PrettyPrinter(JsObject(ListMap(fields.toSeq.sortBy(_._1):_*))) mustEqual {
}

"ignore undefined fields" in {
val fieldsWithUndefined = fields + ("notthere" -> JsUndefined)
PrettyPrinter(JsObject(ListMap(fieldsWithUndefined.toSeq.sortBy(_._1):_*))) mustEqual {
"""{
| "Boolean no": false,
| "Boolean yes": true,
Expand Down
6 changes: 6 additions & 0 deletions src/test/scala/spray/json/ProductFormatsSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,18 @@ class ProductFormatsSpec extends Specification {
"deserialize undefined to Tription `Undefined`" in {
JsObject("a" -> JsNumber(42), "b" -> JsUndefined).convertTo[TestTription] mustEqual TestTription(42, Undefined)
}
"deserialize valued to Tription the value" in {
JsObject("a" -> JsNumber(42), "b" -> JsNumber(4.2D)).convertTo[TestTription] mustEqual TestTription(42, Value(4.2D))
}
"not render `None` members during serialization" in {
Test2(42, None).toJson mustEqual JsObject("a" -> JsNumber(42))
}
"render `Null` members during serialization" in {
TestTription(42, Null).toJson mustEqual JsObject("a" -> JsNumber(42), "b" -> JsNull)
}
"render `Value` members during serialization" in {
TestTription(42, Value(4.2D)).toJson mustEqual JsObject("a" -> JsNumber(42), "b" -> JsNumber(4.2D))
}
"not render `Undefined` members during serialization" in {
TestTription(42, Undefined).toJson mustEqual JsObject("a" -> JsNumber(42))
}
Expand Down

0 comments on commit 60f2687

Please sign in to comment.