diff --git a/core/src/main/scala/sjsonnew/CollectionFormats.scala b/core/src/main/scala/sjsonnew/CollectionFormats.scala index 805f84a..09097b0 100644 --- a/core/src/main/scala/sjsonnew/CollectionFormats.scala +++ b/core/src/main/scala/sjsonnew/CollectionFormats.scala @@ -108,8 +108,8 @@ trait CollectionFormats { case Some(js) => val size = unbuilder.beginObject(js) val xs = (1 to size).toList map { x => - val (k, v) = unbuilder.nextField - keyFormat.read(k) -> valueFormat.read(Some(v), unbuilder) + val (k, v) = unbuilder.nextFieldOpt + keyFormat.read(k) -> valueFormat.read(v, unbuilder) } unbuilder.endObject Map(xs: _*) diff --git a/core/src/main/scala/sjsonnew/LList.scala b/core/src/main/scala/sjsonnew/LList.scala index 7bd7415..1bb4960 100644 --- a/core/src/main/scala/sjsonnew/LList.scala +++ b/core/src/main/scala/sjsonnew/LList.scala @@ -60,27 +60,24 @@ trait LListFormats { import BasicJsonProtocol._ implicit val lnilFormat: JsonFormat[LNil] = new JsonFormat[LNil] { - def write[J](x: LNil, builder: Builder[J]): Unit = - { - if (!builder.isInObject) { - builder.beginObject() - } - builder.endObject() - } - def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): LNil = - { - if (unbuilder.isInObject) { - unbuilder.endObject() - } - LList.LNil0 - } + def write[J](x: LNil, builder: Builder[J]): Unit = { + if (!builder.isInObject) builder.beginObject() + builder.endObject() + } + def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): LNil = { + if (unbuilder.isInObject) unbuilder.endObject() + LNil + } } + private val fieldNamesField = "$fields" - implicit def lconsFormat[A1: JsonFormat: ClassTag, A2 <: LList: JsonFormat]: JsonFormat[LCons[A1, A2]] = new JsonFormat[LCons[A1, A2]] { - val a1Format = implicitly[JsonFormat[A1]] - val a2Format = implicitly[JsonFormat[A2]] - def write[J](x: LCons[A1, A2], builder: Builder[J]): Unit = - { + + implicit def lconsFormat[A1: JsonFormat: ClassTag, A2 <: LList: JsonFormat]: JsonFormat[LCons[A1, A2]] = + new JsonFormat[LCons[A1, A2]] { + val a1Format: JsonFormat[A1] = implicitly + val a2Format: JsonFormat[A2] = implicitly + + def write[J](x: LCons[A1, A2], builder: Builder[J]): Unit = { if (!builder.isInObject) { builder.beginPreObject() builder.addField(fieldNamesField, x.fieldNames) @@ -90,29 +87,32 @@ trait LListFormats { builder.addField(x.name, x.head)(a1Format) a2Format.write(x.tail, builder) } - def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): LCons[A1, A2] = - jsOpt match { - case Some(js) => - def objectPreamble(x: J) = { - unbuilder.beginPreObject(x) - val jf = implicitly[JsonFormat[Vector[String]]] - val fieldNames = unbuilder.lookupField(fieldNamesField).map(x => jf.read(Some(x), unbuilder)) - unbuilder.endPreObject() - unbuilder.beginObject(x, fieldNames) - } - if (!unbuilder.isInObject) objectPreamble(js) - if (unbuilder.hasNextField) { - val (name, x) = unbuilder.nextField - if (unbuilder.isObject(x)) objectPreamble(x) - val elem = a1Format.read(Some(x), unbuilder) - val tail = a2Format.read(Some(js), unbuilder) - LCons(name, elem, tail) - } - else deserializationError(s"Unexpected end of object: $js") - case None => - val elem = a1Format.read(None, unbuilder) - val tail = a2Format.read(None, unbuilder) - LCons("*", elem, tail) - } - } + + def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): LCons[A1, A2] = + jsOpt match { + case Some(js) => + def objectPreamble(x: J) = { + unbuilder.beginPreObject(x) + val jf = implicitly[JsonFormat[Vector[String]]] + val fieldNames = unbuilder.lookupField(fieldNamesField).map(x => jf.read(Some(x), unbuilder)) + unbuilder.endPreObject() + unbuilder.beginObject(x, fieldNames) + } + if (!unbuilder.isInObject) objectPreamble(js) + if (unbuilder.hasNextField) { + val (name, optX) = unbuilder.nextFieldOpt + optX foreach { x => + if (unbuilder.isObject(x)) objectPreamble(x) + } + val elem = a1Format.read(optX, unbuilder) + val tail = a2Format.read(Option(js), unbuilder) + LCons(name, elem, tail) + } + else deserializationError(s"Unexpected end of object: $js") + case None => + val elem = a1Format.read(None, unbuilder) + val tail = a2Format.read(None, unbuilder) + LCons("*", elem, tail) + } + } } diff --git a/core/src/main/scala/sjsonnew/Unbuilder.scala b/core/src/main/scala/sjsonnew/Unbuilder.scala index 1604f94..5f4aef3 100644 --- a/core/src/main/scala/sjsonnew/Unbuilder.scala +++ b/core/src/main/scala/sjsonnew/Unbuilder.scala @@ -148,7 +148,7 @@ class Unbuilder[J](facade: Facade[J]) { case x => stateError(x) } - def nextField(): (String, J) = + def nextFieldOpt(): (String, Option[J]) = state match { case InObject => contexts.head match { @@ -158,7 +158,23 @@ class Unbuilder[J](facade: Facade[J]) { case x => stateError(x) } - def nextFieldWithJString(): (J, J) = nextField match { case (k, v) => (facade.jstring(k), v) } + def nextFieldOptWithJString(): (J, Option[J]) = + nextFieldOpt() match { + case (k, v) => (facade.jstring(k), v) + } + + @deprecated("Use nextFieldOpt that returns (String, Option[J]). nextField uses JNull to encode elided fields.", "0.8.0") + def nextField(): (String, J) = + nextFieldOpt() match { + case (k, Some(v)) => (k, v) + case (k, None) => (k, facade.jnull()) + } + + @deprecated("Use nextFieldOpt that returns (J, Option[J]). nextFieldOptWithJString uses JNull to encode elided fields.", "0.8.0") + def nextFieldWithJString(): (J, J) = + nextField match { + case (k, v) => (facade.jstring(k), v) + } def lookupField(name: String): Option[J] = state match { @@ -221,9 +237,10 @@ private[sjsonnew] object UnbuilderContext { private val size = names.size private var idx: Int = 0 def hasNext: Boolean = idx < size - def next: (String, J) = { + def next: (String, Option[J]) = { val name = names(idx) - val x = fields(names(idx)) + // nulls and empty collections are elided, so it won't show up. + val x: Option[J] = fields.get(names(idx)) idx = idx + 1 (name, x) } diff --git a/support/msgpack/src/main/scala/sjonnew/support/msgpack/Converter.scala b/support/msgpack/src/main/scala/sjonnew/support/msgpack/Converter.scala index 9aa0d64..32ac254 100644 --- a/support/msgpack/src/main/scala/sjonnew/support/msgpack/Converter.scala +++ b/support/msgpack/src/main/scala/sjonnew/support/msgpack/Converter.scala @@ -146,6 +146,8 @@ object Converter extends SupportConverter[Value] { vectorBuilder += array.next() } vectorBuilder.result() + case ValueType.NIL => + Vector() case _ => deserializationError("Expected List as Array, but got " + value) } def extractObject(value: Value): (Map[String, Value], Vector[String]) = @@ -162,6 +164,8 @@ object Converter extends SupportConverter[Value] { mapBuilder += (keyString -> entry.getValue) } (mapBuilder.result(), vectorBuilder.result()) + case ValueType.NIL => + (Map.empty, Vector.empty) case _ => deserializationError("Expected Map as MMap, but got " + value) } } diff --git a/support/scalajson/src/main/scala/sjsonnew/support/scalajson/unsafe/Converter.scala b/support/scalajson/src/main/scala/sjsonnew/support/scalajson/unsafe/Converter.scala index 93e9dbb..fc14762 100644 --- a/support/scalajson/src/main/scala/sjsonnew/support/scalajson/unsafe/Converter.scala +++ b/support/scalajson/src/main/scala/sjsonnew/support/scalajson/unsafe/Converter.scala @@ -100,6 +100,7 @@ object Converter extends SupportConverter[JValue] { def extractArray(value: JValue): Vector[JValue] = value match { case JArray(elements) => elements.toVector + case JNull => Vector.empty case x => deserializationError("Expected List as JArray, but got " + x) } def extractObject(value: JValue): (Map[String, JValue], Vector[String]) = @@ -108,6 +109,8 @@ object Converter extends SupportConverter[JValue] { val names = (fs map { case JField(k, v) => k }).toVector val fields = Map((fs map { case JField(k, v) => (k, v) }): _*) (fields, names) + case JNull => + (Map.empty, Vector.empty) case x => deserializationError("Expected Map as JsObject, but got " + x) } } diff --git a/support/scalajson/src/test/scala/sjsonnew/support/scalajson/unsafe/LListFormatSpec.scala b/support/scalajson/src/test/scala/sjsonnew/support/scalajson/unsafe/LListFormatSpec.scala new file mode 100644 index 0000000..7d56736 --- /dev/null +++ b/support/scalajson/src/test/scala/sjsonnew/support/scalajson/unsafe/LListFormatSpec.scala @@ -0,0 +1,32 @@ +package sjsonnew +package support.scalajson.unsafe + +import shaded.scalajson.ast.unsafe._ + +import org.scalatest.FlatSpec + +import BasicJsonProtocol._ + +final class LListFormatSpec extends FlatSpec { + case class Foo(xs: Seq[String]) + + implicit val isoLList: IsoLList[Foo] = LList.isoCurried( + (a: Foo) => "xs" -> a.xs :*: LNil + ) { case (_, xs) :*: LNil => Foo(xs) } + + val foo = Foo(Nil) + val fooLList = "xs" -> List.empty[String] :*: LNil + val fooJson = JObject(JField("$fields", JArray(JString("xs")))) + val fooJsonStr = """{"$fields":["xs"]}""" + + it should "Foo -> LList" in assert((isoLList to foo) === fooLList) + it should "Foo -> JSON" in assert(foo.toJson === fooJson) + it should "Foo -> JSON string" in assert(foo.toJsonStr === fooJsonStr) + it should "JSON string -> JSON" in assert(fooJsonStr.toJson === fooJson) + it should "JSON string -> Foo" in assert(fooJsonStr.fromJsonStr[Foo] === foo) + it should "round trip" in assertRoundTrip(foo) + it should "round trip pretty" in assertPrettyRoundTrip(foo) + + private def assertRoundTrip[A: JsonWriter : JsonReader](x: A) = assert(x === x.jsonRoundTrip) + private def assertPrettyRoundTrip[A: JsonWriter : JsonReader](x: A) = assert(x === x.jsonPrettyRoundTrip) +} diff --git a/support/spray/src/main/scala/sjsonnew/support/spray/Converter.scala b/support/spray/src/main/scala/sjsonnew/support/spray/Converter.scala index f82b3cd..8d74a8a 100644 --- a/support/spray/src/main/scala/sjsonnew/support/spray/Converter.scala +++ b/support/spray/src/main/scala/sjsonnew/support/spray/Converter.scala @@ -70,6 +70,7 @@ object Converter extends SupportConverter[JsValue] { def extractArray(value: JsValue): Vector[JsValue] = value match { case JsArray(elements) => elements + case JsNull => Vector.empty case x => deserializationError("Expected List as JsArray, but got " + x) } def extractObject(value: JsValue): (Map[String, JsValue], Vector[String]) = @@ -81,6 +82,8 @@ object Converter extends SupportConverter[JsValue] { vectorBuilder += field._1 } (fields, vectorBuilder.result()) + case JsNull => + (Map.empty, Vector.empty) case x => deserializationError("Expected Map as JsObject, but got " + x) } }