Play JSON with sealed trait / case classes: infinite recursion - scala

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

Scala : Add an implicit method to transform case class to another case class in alias object?

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()

Generic function for case class creation with tuple does not compile

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 to serialize sealed abstract class with Json4s in Scala?

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))

Scala: handle different constructor when extending a parameterised Class

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()

Get field names from Scala case class with specific annotation

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)
}