How do i serialize a sealed abstract class with Json4s in Scala?
The following classes are defined:
sealed abstract class Person extends Product with Serializable
case class Spouse(name: String, age: Int) extends Person
case class Customer(name: String, age: Int, spouse: Spouse) extends Person
I create an object of type Customer:
val customer: Customer = Customer("Joe", 35, Spouse("Marilyn", 33))
Then I serialize to JSON:
implicit val formats = DefaultFormats
val serialized = write(customer)
That works fine. But then I try to deserialize:
val parsedObj = Serialization.read[Person](serialized)
Here I keep getting error:
org.json4s.package$MappingException: Parsed JSON values do not match with class constructor
I spent a lot of time trying to make this work...
After a lot of googling around and a close read of the Json4s documentation i found out that I need a custom Serializer to make it work.
However, it took me a while to figure out that I need to set the formats to actually use the custom Serializer.
Here is the code that works for me.
Simple Example:
import org.json4s._
import org.json4s.native.JsonMethods._
import org.json4s.native.Serialization.{read, write}
import org.json4s.native.Serialization
sealed abstract class Person extends Product with Serializable
case class Spouse(name: String, age: Int) extends Person
case class Customer(name: String, age: Int, spouse: Spouse) extends Person
val customer: Customer = Customer("Joe", 35, Spouse("Marilyn", 33))
implicit val formats = Serialization.formats(NoTypeHints) + PersonSerializer
val serialized = write(customer)
val parsedObj = Serialization.read[Person](serialized)
Custom Serializer:
object PersonSerializer extends Serializer[Person] {
private val PersonClass = classOf[Person]
def deserialize(implicit format: Formats)
: PartialFunction[(TypeInfo, JValue), Person] = {
case (TypeInfo(PersonClass, _), json) =>
json match {
case JObject(List(
JField("name", JString(d)),
JField("age", JInt(f)),
("spouse", JObject(List(JField("name", JString(g)), JField("age", JInt(h)))))
)) => Customer(d, f.toInt, Spouse(g, h.toInt))
case JObject(List(
JField("name", JString(d)),
JField("age", JInt(f))
)) => Spouse(d, f.toInt)
}
}
def serialize(implicit format: Formats): PartialFunction[Any, JValue] = {
case x: Customer =>
JObject(List(
JField("name", JString(x.name)),
JField("age", JInt(x.age)),
("spouse", JObject(List(JField("name", JString(x.spouse.name)), JField("age", JInt(x.spouse.age)))))
))
case x: Spouse =>
JObject(List(
JField("name", JString(x.name)),
JField("age", JInt(x.age))
))
}
}
Output
scala> val serialized = write(customer)
serialized: String = {"name":"Joe","age":35,"spouse":{"name":"Marilyn","age":33}}
scala> val parsedObj = Serialization.readPerson
parsedObj: Person = Customer(Joe,35,Spouse(Marilyn,33))
Related
I have a case class with a bunch of fields, two of them are trait and have children classes. Something like this:
trait Trait1
case class T11(a: String) extends Trait1
case class T12(b: Int, c: Double) extends Trait1
trait Trait2
case class T21(a: Int, b: Int) extends Trait2
case class T22(a: Double, b: String, c: Boolean) extends Trait2
case class F1(...)
case class F2(...)
case class CaseClass(field1: F1, field2: F2, field3: Option[Trait1] = None, field4: Option[Trait2] = None)
For backward compatibility, I need to make sure the content of Trait1 and Trait2 are serialized to json at the top level of my class CaseClass. i.e. something like this
{
"field1": {...},
"field2": {...},
"a1": ...,
"a2": ...,
"b2": ...
}
I tried with the following logic to serialize and deserialize my class CaseClass as well as the other traits and their children classes
object Trait1 {
implicit val format: OFormat[Trait1] = {
implicit def format1: OFormat[T11] = Json.format[T11]
implicit def format2: OFormat[T12] = Json.format[T12]
Json.format[Trait1]
}
}
object Trait2 {
implicit val format: OFormat[Trait2] = {
implicit def format1: OFormat[T21] = Json.format[T21]
implicit def format2: OFormat[T22] = Json.format[T22]
Json.format[Trait2]
}
}
object T21 {
implicit val format = Json.format[T21]
}
object T22 {
implicit val format = Json.format[T22]
}
object CaseClass {
implicit val reads: Reads[CaseClass] = (JsPath \ "field1").read[F1]
.and((JsPath \ "field2").read[F2])
.and(JsPath.readNullable[Trait1])
.and(JsPath.readNullable[Trait2])
.apply(CaseClass.apply _)
implicit val writes: Writes[CaseClass] =
(JsPath \ "field1").write[F1]
.and((JsPath \ "field2").write[F2])
.and(JsPath.writeNullable[Trait1])
.and(JsPath.writeNullable[Trait2])
.apply(unlift(CaseClass.unapply))
}
With this logic I can serialize the class into the target json as wanted with Json.toJson, but i tried to deseriliaze that Json back to classes with Json.parse it fails with something like this
JsResultException(errors:List((,List(JsonValidationError(List(error.invalid),WrappedArray())))))
play.api.libs.json.JsResultException: JsResultException(errors:List((,List(JsonValidationError(List(error.invalid),WrappedArray())))))
at play.api.libs.json.JsReadable.$anonfun$as$2(JsReadable.scala:25)
at play.api.libs.json.JsError.fold(JsResult.scala:64)
at play.api.libs.json.JsReadable.as(JsReadable.scala:24)
at play.api.libs.json.JsReadable.as$(JsReadable.scala:23)
at play.api.libs.json.JsObject.as(JsValue.scala:124)
at dzlab.CaseClassTest.jsonReadComparison(CaseClassTest.scala:45)
I have a code where I try to customize JSON serialization of a bunch of case classes by defining a custom Writes for the base trait. I'm getting infinite recursion / stack overflow.
I created a simplified sample - if somebody knows how to fix it, please let me know.
import play.api.libs.json._
sealed trait Person {
val name: String
}
final case class Teacher(name: String, salary: Int) extends Person
final case class Student(name: String, grade: Int) extends Person
implicit val teacherWrites: Writes[Teacher] = Json.writes[Teacher]
implicit val studentWrites: Writes[Student] = Json.writes[Student]
val ThePersonWrites: Writes[Person] = Writes(person => {
Json.writes[Person].writes(person).as[JsObject] - "_type"
})
implicit val personWrites: Writes[Person] = ThePersonWrites
val people = List[Person] (
Teacher("Jane Doe", 40000),
Student("Alice", 5),
Student("Bob", 7)
)
Json.prettyPrint(Json.toJson(people))
You need play-json-derived-codecs
import play.api.libs.json._
import julienrf.json.derived
sealed trait Person {
val name: String
}
object Person {
implicit val jsonFormat: OFormat[Person] = derived.oformat[Person]()
}
final case class Teacher(name: String, salary: Int) extends Person
final case class Student(name: String, grade: Int) extends Person
val people = List[Person] (
Teacher("Jane Doe", 40000),
Student("Alice", 5),
Student("Bob", 7)
)
println(Json.prettyPrint(Json.toJson(people)))
See here the scalafiddle
This should do it:
import play.api.libs.json._
sealed trait Person {
val name: String
}
final case class Teacher(name: String, salary: Int) extends Person
final case class Student(name: String, grade: Int) extends Person
implicit val teacherWrites: Writes[Teacher] = Json.writes[Teacher]
implicit val studentWrites: Writes[Student] = Json.writes[Student]
implicit val personWrites: Writes[Person] = Writes[Person] {
case t: Teacher => Json.toJson(t)(teacherWrites)
case s: Student => Json.toJson(s)(studentWrites)
}
val people = List[Person] (
Teacher("Jane Doe", 40000),
Student("Alice", 5),
Student("Bob", 7)
)
Json.prettyPrint(Json.toJson(people))
The trick is adding teacherWrites and studentWrites explicitly. Because they are both Persons, before it was recognizing them as such and calling your personWrites again, hence the stack overflow.
I wanted to serialize and deserialize some case classes and realized I was repeating code. Unfortunately I cannot figure out a way to keep things DRY. Hoping someone can provide some assistance. Below I will provide a sample problem that is not DRY.
Sample Problem
import org.json4s.jackson.Serialization
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import org.json4s.JsonAST.JString
import org.json4s.{CustomSerializer, DefaultFormats}
case class Bar(bar: String, date: ZonedDateTime)
case class Foo(foo: String)
trait JsonParser {
private case object ZDTSerializer extends CustomSerializer[ZonedDateTime](_ => (
{ case JString(s) => ZonedDateTime.parse(s) },
{ case zdt: ZonedDateTime => JString(zdt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"))) }
))
implicit val formats = DefaultFormats + ZDTSerializer
}
object BarParser extends JsonParser {
def deserialize(jsonBar: String): Bar = {
Serialization.read[Bar](jsonBar)
}
def serialize(bar: Bar): String = {
Serialization.write[Bar](bar)
}
}
object FooParser extends JsonParser {
def deserialize(jsonFoo: String): Foo = {
Serialization.read[Foo](jsonFoo)
}
def serialize(foo: Foo): String = {
Serialization.write[Foo](foo)
}
}
object Main {
def main(args: Array[String]): Unit = {
val foo = Foo("foo")
println(FooParser.serialize(foo)) // {"foo":"foo"}
println(FooParser.deserialize(FooParser.serialize(foo))) // Foo(foo)
}
}
Above it is clear that the logic to serialize and deserialize is repeated. This is one of the things I've tried (which doesn't compile).
Attempt to Solve
case class Bar(product: String, date: ZonedDateTime)
case class Foo(title: String)
abstract class GenericJsonParser[T] {
private case object ZDTSerializer extends CustomSerializer[ZonedDateTime](_ => (
{ case JString(s) => ZonedDateTime.parse(s) },
{ case zdt: ZonedDateTime => JString(zdt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"))) }
))
implicit val formats = DefaultFormats + ZDTSerializer
def deserialize(json: String): T = {
Serialization.read[T](json) // No Manifest available for T
}
def serialize(thing: T): String = {
Serialization.write[T](thing) // type arguments [A] conform to the bounds of none of the overloaded alternatives ...
}
}
object BarJsonParser extends GenericJsonParser[Bar]
object FooParser extends GenericJsonParser[Foo]
Any guidance would be appreciated.
I think you can use Json.format[ACaseClass], for example:
import play.api.libs.json.{Format, Json}
case class ACaseClass(value: String, anotherValue: Int)
implicit val formatACaseClass = Json.format[ACaseClass]
I guess for Seralization.read and write you still have to pass an implicit value, jvm need must know how to read/write your object/
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.
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)
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)
Suppose I have an enumeration or sealed group of case objects as follows:
sealed abstract class Status
case object Complete extends Status
case object Failed extends Status
case object Pending extends Status
case object Unknown extends Status
or
object Status extends Enumeration {
val Complete, Failed, Pending, Unknown = Value
}
What is the easiest way to create json formats for these so that I can very easily (programmatically) generate json formats for use in a custom JsonFormat factory method, such as the following, which works for all normal case classes, strings, collections, etc., but produces {} or {"name": null} for the above two types of enumerations?:
import org.json4s.DefaultFormats
import org.json4s.jackson.JsonMethods.parse
import org.json4s.jackson.Serialization
import org.json4s.jvalue2extractable
import org.json4s.string2JsonInput
trait JsonFormat[T] {
def read(json: String): T
def write(t: T): String
}
object JsonFormat {
implicit lazy val formats = DefaultFormats
def create[T <: AnyRef: Manifest](): JsonFormat[T] = new JsonFormat[T] {
def read(json: String): T = parse(json).extract[T]
def write(t: T): String = Serialization.write(t)
}
}
We've used org.json4s.ext.EnumNameSerializer to serialize enumerations:
import org.json4s._
import org.json4s.ext.EnumNameSerializer
class DoesSomething {
implicit lazy val formats = DefaultFormats + new EnumNameSerializer(Status)
...stuff requiring serialization or deserialization...
}
In practice we have mixin trait that adds the implicit format and defines all of our custom serializer/desrializers:
trait OurFormaters extends Json4sJacksonSupport {
implicit lazy val json4sJacksonFormats:Formats = DefaultFormats +
UuidSerializer +
new EnumNameSerializer(Status) +
...
}
object UuidSerializer extends CustomSerializer[UUID](format =>
(
{
case JString(s) => UUID.fromString(s)
case JNull => null
},
{
case x: UUID => JString(x.toString)
}
)
)