4
votes

Using Spray with spray-json for a system, version:

"io.spray" %% "spray-json" % "1.2.6"

I cannot figure how to get custom JsonFormat definitions to work for serialization that is being handled by spray-routing.

I've had two separate circumstances that have failed.

1. Nested Case Classes

Basic case class JSON serialization has worked fine

case class Something(a: String, b: String)
implicit val something2Json = jsonFormat3(Something)

However if I have a nested case class in the case class to be serialized, I can resolve compile issues by providing another JsonFormat implicit, yet at run-time it refuses to serialize

case class Subrecord(value: String)
case class Record(a: String, b: String, subrecord: Subrecord)

object MyJsonProtocol extends DefaultJsonProtocol {
  implicit object SubrecordJsonFormat extends JsonFormat[Subrecord] {
    def write(sub: Subrecord) = JsString(sub.value)
    def read(value: JsValue) = value match {
      case JsString(s) => Subrecord(s)
      case _ => throw new DeserializationException("Cannot parse Subrecord")
    }
  }

  implicit val record2Json = jsonFormat3(Record)
}

This will throw a MappingException at runtime, explaining there is no usable value for subrecord

2. Trait with various 0-N case extensions

Here I have a trait that serves as a capturing type for a group of case classes. Some of the extending classes have vals while others have no vals and are objects. When serialization occurs, it seems like my implicit defined JsonFormat is completely ignored and I'm just give an empty JsObject, particularly when the actual underlying type was one of the case object's with no vals.

sealed trait Errors
sealed trait ErrorsWithReason extends Errors {
  def reason: String
}

case class ValidationError(reason: String) extends ErrorsWithReason
case object EntityNotFound extends Errors
case class DatabaseError(reason: String) extends ErrorsWithReason

object MyJsonProtocol extends DefaultJsonProtocol {
  implicit object ErrorsJsonFormat extends JsonFormat[Errors] {
    def write(err: Errors) = failure match {
      case e: ErrorsWithReason => JsString(e.reason)
      case x => JsString(x.toString())
    }
    def read(value: JsValue) = {
      value match {
        //Really only intended to serialize to JSON for API responses
        case _ => throw new DeserializationException("Can't reliably deserialize Error")
      }
    }
  }
}

So given the above, if the actual type being serialized is EntityNotFound, then the serialization becomes a RootJsonFormat turning into {}. If it's an ErrorsWithReason then it becomes a RootJsonFormat turning into { "reason": "somevalue" }. I may be confused with how the JsonFormat definition is supposed to work, but it doesn't seem to be using my write method at all and instead has suddenly figured out how to serialize on its own.

EDIT

Specific serialization cases are using read/deserialization like:

entity(as[JObject]) { json =>
  val extraction: A = json.extract[A]
}

And write/serialization with the complete directive.

I now am realizing thanks to the first answer posted here that my JsonDefaultProtocol and JsonFormat implementations are for spray-json classes, meanwhile the entity directive extraction in the deserialization is using json4s JObject as opposed to spray-json JsObject.

2
Are you putting your implicit in an object that extends DefaultJsonProtocol and then importing the members from that object? Example: github.com/spray/…Gangstead
Once you can serialize the inner class using your extension to the jsonprotocol object. Then create an object for the outer case class and import the members of the inner object into it.Gangstead
Sorry, I will edit the question to reflect this but yes, all of my implicits are in a DefaultJsonProtocol extension that is imported.Rich

2 Answers

3
votes

Another approach for clean JSON output

  import spray.json._
  import spray.json.DefaultJsonProtocol._

  // #1. Subrecords
  case class Subrecord(value: String)
  case class Record(a: String, b: String, subrecord: Subrecord)

  implicit object RecordFormat extends JsonFormat[Record] {
    def write(obj: Record): JsValue = {
      JsObject(
        ("a", JsString(obj.a)),
        ("b", JsString(obj.b)),
        ("reason", JsString(obj.subrecord.value))
      )
    }

    def read(json: JsValue): Record = json match {
      case JsObject(fields)
        if fields.isDefinedAt("a") & fields.isDefinedAt("b") & fields.isDefinedAt("reason") =>
          Record(fields("a").convertTo[String],
            fields("b").convertTo[String],
            Subrecord(fields("reason").convertTo[String])
          )

      case _ => deserializationError("Not a Record")
    }

  }


  val record = Record("first", "other", Subrecord("some error message"))
  val recordToJson = record.toJson
  val recordFromJson = recordToJson.convertTo[Record]

  println(recordToJson)
  assert(recordFromJson == record)
1
votes

If you need both reads and writes you can do it this way:

  import spray.json._
  import spray.json.DefaultJsonProtocol._

  // #1. Subrecords
  case class Subrecord(value: String)
  case class Record(a: String, b: String, subrecord: Subrecord)

  implicit val subrecordFormat = jsonFormat1(Subrecord)
  implicit val recordFormat = jsonFormat3(Record)

  val record = Record("a", "b", Subrecord("c"))
  val recordToJson = record.toJson
  val recordFromJson = recordToJson.convertTo[Record]

  assert(recordFromJson == record)

  // #2. Sealed traits

  sealed trait Errors
  sealed trait ErrorsWithReason extends Errors {
    def reason: String
  }

  case class ValidationError(reason: String) extends ErrorsWithReason
  case object EntityNotFound extends Errors
  case class DatabaseError(reason: String) extends ErrorsWithReason

  implicit object ErrorsJsonFormat extends JsonFormat[Errors] {
    def write(err: Errors) = err match {
      case ValidationError(reason) =>
        JsObject(
        ("error", JsString("ValidationError")),
        ("reason", JsString(reason))
      )
      case DatabaseError(reason) =>
        JsObject(
          ("error", JsString("DatabaseError")),
          ("reason", JsString(reason))
        )
      case EntityNotFound => JsString("EntityNotFound")
    }

    def read(value: JsValue) = value match {
      case JsString("EntityNotFound") => EntityNotFound
      case JsObject(fields) if fields("error") == JsString("ValidationError") =>
         ValidationError(fields("reason").convertTo[String])
      case JsObject(fields) if fields("error") == JsString("DatabaseError") =>
        DatabaseError(fields("reason").convertTo[String])
    }
  }

  val validationError: Errors = ValidationError("error")
  val databaseError: Errors = DatabaseError("error")
  val entityNotFound: Errors = EntityNotFound

  assert(validationError.toJson.convertTo[Errors] == validationError)
  assert(databaseError.toJson.convertTo[Errors] == databaseError)
  assert(entityNotFound.toJson.convertTo[Errors] == entityNotFound)