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.
Related
I am new to Scala and I have a case class like this:
case class Student(
name: String,
age: Option[Int] = None,
id: String
)
and another case class like this:
case class Member(
id: String,
`type`: String,
name: String
)
I want to create a helper implicit in such a way that:
case class Member(
id: String,
`type`: String,
name: String
)
object Member {
implicit def toStudent(member:Member):Student = Student(member.name,None,member.id)
}
and then call the method like this:
val newMember = Member("abc","Student","John")
val student = newMember.toStudent
or something fancy like that. Would this be possible in Scala as I see a lot of fancy syntax similar to this at a lot of places?
In Scala 3 you can use extensions methods:
// ...
extension (member: Member)
def toStudent: Student = Student(member.name, None, member.id)
val newMember = Member("abc", "Student", "John")
val student = newMember.toStudent
For Scala 2 you can define explicit conversion:
import scala.language.implicitConversions
object Member {
implicit def toStudent(member: Member): Student =
Student(member.name, None, member.id)
}
val newMember = Member("abc", "Student", "John")
val student = newMember: Student
Or using implicit class decorator:
implicit class MemberDecorator(val member: Member) extends AnyVal {
def toStudent(): Student = Student(member.name, None, member.id)
}
val newMember = Member("abc", "Student", "John")
val student = newMember.toStudent()
I shall explain my question with example as shown below.
import scala.reflect.ClassTag
trait LivingBeing extends Product { def name:String; def age:Int}
case class Person (name:String, age:Int) extends LivingBeing
case class Cat(name: String, age:Int) extends LivingBeing
// usual way of creating a case class instance
val john = Person("john", 23)
// Creating a case class instance with tuples
val garfield = Cat tupled ("Garfield", 8)
// create a generic function
def createLivingBeing[T<: LivingBeing](name:String, age:Int)(implicit evidence: ClassTag[T]): T = {
T tupled (name, age) // Does not compile; why?
}
How can one elegantly construct different case classes (that are of a certain trait) generically, given a type and values for its fields?
Consider type class solution
trait LivingBeingFactory[T <: LivingBeing] {
def apply(name: String, age: Int): T
}
object LivingBeingFactory {
implicit val personFactory: LivingBeingFactory[Person] =
(name: String, age: Int) => Person(name, age)
implicit val catFactory: LivingBeingFactory[Cat] =
(name: String, age: Int) => Cat(name, age)
}
def createLivingBeing[T <: LivingBeing](name:String, age:Int)(implicit evidence: LivingBeingFactory[T]): T = {
evidence(name, age)
}
createLivingBeing[Person]("Picard", 70)
// res0: Person = Person(Picard,70)
createLivingBeing[Cat]("Bob", 5)
// res1: Cat = Cat(Bob,5)
// Creating a case class instance with tuples
val garfield = Cat tupled ("Garfield", 8)
...which is the equivalent of...
val garfield = (Cat.apply _).tupled(("Garfield", 8))
This, on the other hand...
T tupled (name, age) // Does not compile; why?
...produces Error: not found: value T because T is a type, not a value. Cat is both a type and a value. It is the type specified for the class, but it is also the companion object to the class Cat. All case classes have a companion object with an apply() method. The compiler knows the difference between them and it knows where one can be used/referenced but not the other.
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))
I am writing a scala code, and want to handle different constructor when extending a parameterised Class. For example:
class Person (val name:String, val age: Int)
class Employee(name:String, age:Int, val position:String)
extends Person(name, age)
However, what I want is that Employee can have two constructors one of which takes information of Person to construct, one takes another Employee to construct:
val emply1 = new Employee(yzh, 30, CEO)
val emply2 = new Employee(emply1)
If I want both constructors works, how can I do?
Thanks!
If you want two constructors, you simply write two constructors:
class Person(val name: String, val age: Int)
class Employee(name: String, age: Int, val position: String) extends
Person(name, age) {
def this(e: Employee) = this(e.name, e.age, e.position)
}
val emply1 = new Employee("yzh", 30, "CEO")
val emply2 = new Employee(emply1)
You need to add an axillary constructor.
class Employee(name:String, age:Int, val position:String)
extends Person(name, age) {
def this(emp: Employee) = this(emp.name, emp.age, emp.position)
}
If your Employee were a case class then you could do this instead.
val emply1 = Employee("yzh", 30, "CEO")
val emply2 = emply1.copy()
final class ContactInfo extends StaticAnnotation{}
case class Person(val id: String,
val firstName: String,
val lastName: String,
#ContactInfo val phoneNumbers: Seq[String],
#ContactInfo val email: String)
def getContactInfoFields[T: TypeTag]: Seq[String] = {
???
}
Expected output getContactInfoFields[Person] = ("phoneNumbers", "email")
Riffing off the answer to a similar question on SO I've tried
def getContactInfoFields[T: TypeTag]: Seq[String] = {
val fields = typeOf[T].members.collect{ case s: TermSymbol => s }.
filter(s => s.isVal || s.isVar)
fields.filter(_.annotations.exists(_.isInstanceOf[ContactInfo]))
.map(x=>x.name.toString).toSeq
}
However in practice this is returning an empty sequence. What am I missing?
You could represent this information at the type level.
sealed trait ContactInfo
case class PhoneNumbers(numbers: Seq[String]) extends ContactInfo
case class Email(email: String) extends ContactInfo
case class Person(id: String, firstName: String, lastName: String, phoneNumbers: PhoneNumbers, email: Email)
def contactInfo[T: TypeTag] = typeOf[T].members.filter(!_.isMethod).map(_.typeSignature).collect {
case t if t <:< typeOf[ContactInfo] => t.typeSymbol.name.toString
}
Calling contactInfo[Person] returns Iterable[String] = List(Email, PhoneNumbers)
Thank you everyone for all your help! I've managed to come up with a working solution. As it turns out I was trying to compare JavaUniverse.reflection.Types to Scala Universe reflections types which is why the filter statement was failing. The below function returns as expected
def listContactInfoFields(symbol: TypeSymbol): Seq[String] = {
val terms = symbol.asClass.primaryConstructor.typeSignature.paramLists.head
val annotatedFields = terms.filter(_.annotations.exists(_.tree.tpe =:= typeOf[ContactInfo]))
annotatedFields.map(_.name.toString)
}