Spray json serialization in spray routing using custom JsonFormats

Using a spray system for the system, version:

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

      

I can't figure out how to get custom JsonFormat definitions to work for serialization that is handled with spray routing.

I had two separate circumstances that failed.

1. Nested class classes

JSON serialization main class works fine

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

      

However, if I have a nested case class in a case class that will be serialized, I can solve compilation problems by providing another JsonFormat implicitly, but at runtime 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 that there is no useful value for subrecord

2. Trait with various extensions 0-N extension

Here I have a trait that serves as a capture type for a group of case classes. Some expanding classes have shafts, while others have no shafts and have objects. When serialization happens, it seems that my implicit JsonFormat is completely ignored and I just give an empty JsObject, especially if the actual base type was one of the case objects without 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 serialization becomes RootJsonFormat turning into {}

. If it is ErrorsWithReason then it becomes RootJsonFormat turning into { "reason": "somevalue" }

. I might be confused about how the JsonFormat definition is supposed to work, but it doesn't seem to use my write method at all and instead suddenly figured out how to serialize on its own.

EDIT

In specific cases of serialization, read / deserialization is used, for example:

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

      

And write / serialize using a directive complete

.

Now I understand, thanks to the first answer posted here, that my JsonDefaultProtocol and JsonFormat implementations are for spray-json classes, while deleting the entity directive in deserialization uses jon4s JObject, as opposed to JsObject for spray-jsObject.

+3


source to share


2 answers


Another approach for pure 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)

      

+2


source


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



  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)

      

+1


source







All Articles