I have a class that looks somewhat like that
import java.time.OffsetDateTime
import spray.json._
import DefaultJsonProtocol._
sealed trait Person {
def firstName: String
def country: String
def lastName: String
def salary: Option[BigDecimal]
}
case class InternalPerson(
firstName: String,
country: String,
lastName: Option[BigDecimal],
salary: Option[BigDecimal]
) extends Person
object Person {
def fromName(name: Name, country: String, salary: Option[BigDecimal]): Person = {
InternalPerson(
firstName = name.firstName,
lastName = name.lastName,
country = country,
salary = salary
)
}
}
object PersonJsonProtocol extends DefaultJsonProtocol {
implicit val personFormat = jsonFormat4(Person.apply)
}
I am just trying to add a json support to my class. whenever I import the protocol and spray.json._ from another classes I get:
Note: implicit value personFormat is not applicable here because it comes after the application point and it lacks an explicit result type
and
value apply is not a member of object of Person
any idea on how to have Json support for companion objects that extends a trait in scala?
If you defined your implicit in terms of the case class, InternalPerson, json formatting should be enabled: implicit val personFormat = jsonFormat4(InternalPerson)
And you won't have to define an apply() method, which you will have to do in either the Person trait or any implementation thereof otherwise.
You can use play framework to deal with Json.
https://www.playframework.com/documentation/2.6.x/ScalaJson
I think it's very easy and intuitive.
For primitive types it's enough to use Json.format[ClassName]. If you have something more complex you can write your own writes and reads. I know that this question is about spray, but another solution can be good.
So for example for InternalPerson it'll be:
import play.api.libs.json.Json
case class InternalPerson {
firstName: String,
country: String,
lastName: Option[BigDecimal],
salary: Option[BigDecimal]
)
object InternalPerson {
implicit val format = Json.format[InternalPerson]
}
In case that you want to do it with Trait it will be the same. Sometimes you need to write explicitly reads and writes.
Related
I am learning about Scala case classes, and design patterns. To this end I have created an example below which I think is a fairly likely scenario when working with Json type data. I know libraries exist that do this stuff, but I am doing it manually to learn Scala approaches to solving problems, as using a library won't help me learn.
The main design improvement I want to do is abstracting common code.
Suppose my codebase consists of many case classes, where each case class is serializable:
trait Base {
def serialize(): String
}
trait Animal extends Base
trait Mammal extends Animal
trait Reptile extends Animal
case class Lizard(name: String, tail: Boolean) extends Reptile {
def serialize(): String = s"""{name: $name, tail: $tail}"""
}
case class Cat(name: String, age: Int) extends Mammal {
def serialize(): String = s"""{name: $name, age: $age}"""
}
case class Fish(species: String) extends Animal {
def serialize(): String = s"""{species: $species}"""
}
case class Pets(group_name: String, cat: Option[Cat] = None, lizard: Option[Lizard] = None, fish: Fish) extends Base {
def serialize(): String = {
// cat and lizard serialize in a similar fashion
val cat_object = cat match {
case Some(c) => s"""cats: ${c.serialize()}"""
case _ => ""
}
val lizard_object = lizard match {
case Some(d) => s"""lizards: ${d.serialize()}"""
case _ => ""
}
// fish serializes in a different way as it is not an option
val fish_object = s"""fish: ${fish.serialize()}"""
s"""{$lizard_object, $cat_object, $fish_object}"""
}
}
val bob = Cat("Bob", 42)
val jill = Lizard("Jill", true)
val pets = Pets("my group", Some(bob), Some(jill), Fish("goldfish")).serialize()
println(pets)
}
Now there is a repetitive pattern here:
In Pets, when I am serializing, I am basically going over each (key, value) pair (except group_name) in the parameter list and doing the following:
key: value.serialize()
Now I do not know the form of value, it can be an option as in the example. Furthermore, suppose I have many classes like Pets. In that case I would have to manually write many pattern matches on every argument where required, distinguishing between String, Int, Option[String], etc. Would there be a way to abstract out this serializable operation so that if I have many case classes like Pets, I can simply run a single function and get the correct result.
I asked a related question here about getting declared field from cases classes, but it seems that way is not type safe and may create issues later down the line if I add more custom case classes:
https://stackoverflow.com/questions/62662417/how-to-get-case-class-parameter-key-value-pairs
This is a tricky thing to do generically. This code does not always use all the fields in the output (e.g. group_name) and the name of the field does not always match the name in the string (e.g cat vs cats)
However there are some Scala tricks that can make the existing code a bit cleaner:
trait Base {
def serial: String
}
trait Animal extends Base
trait Mammal extends Animal
trait Reptile extends Animal
case class Lizard(name: String, tail: Boolean) extends Reptile {
val serial: String = s"""{name: $name, tail: $tail}"""
}
case class Cat(name: String, age: Int) extends Mammal {
val serial: String = s"""{name: $name, age: $age}"""
}
case class Fish(species: String) extends Animal {
val serial: String = s"""{species: $species}"""
}
case class Pets(group_name: String, cat: Option[Cat] = None, lizard: Option[Lizard] = None, fish: Fish) extends Base {
val serial: String = {
// cat and lizard serialize in a similar fashion
val cat_object = cat.map("cats: " + _.serial)
val lizard_object = lizard.map("lizards: " + _.serial)
// fish serializes in a different way as it is not an option
val fish_object = Some(s"""fish: ${fish.serial}""")
List(lizard_object, cat_object, fish_object).flatten.mkString("{", ", ", "}")
}
}
val bob = Cat("Bob", 42)
val jill = Lizard("Jill", true)
val pets = Pets("my group", Some(bob), Some(jill), Fish("goldfish")).serial
println(pets)
Since the case class is immutable the serialized value does not change, so it makes more sense to make it look like a property called serial.
Option values are best processed inside the Option using map and then extracted at the end. In this case I have used flatten to turn a List[Option[String]] into a List[String].
The mkString method is a good way of formatting lists and avoiding , , in the output if one of the options is empty.
Here's a generic way to make a limited serialization method for case classes, taking advantage of the fact that they're Products.
def basicJson(a: Any): String = a match {
case Some(x) => basicJson(x)
case None => ""
case p: Product =>
(p.productElementNames zip p.productIterator.map(basicJson _))
.map(t => s"${t._1}: ${t._2}")
.mkString("{", ", ", "}")
case _ => a.toString
}
And if you want to serialize Pets without the group name, you could define a single val in Pets that manually serializes all the fields except group_name:
val toJson = s"{cat: ${basicJson(cat)}, lizard: ${basicJson(lizard)}, fish: ${basicJson(fish)}}"
The output of this code
val bob = Cat("Bob", 42)
val jill = Lizard("Jill", true)
val pets = Pets("my group", Some(bob), Some(jill), Fish("goldfish"))
println(pets.toJson)
is this:
{cat: {name: Bob, age: 42}, lizard: {name: Jill, tail: true}, fish: {species: goldfish}}
In Scastie: https://scastie.scala-lang.org/A5slCY65QIKJ2YTNBUPvQA
<script src="https://scastie.scala-lang.org/A5slCY65QIKJ2YTNBUPvQA.js"></script>
Keep in mind that this won't work for anything other than case classes - you'll have to use reflection.
Suppose I have the following abstract base class:
package Models
import reactivemongo.bson.BSONObjectID
abstract class RecordObject {
val _id: String = BSONObjectID.generate().stringify
}
Which is extended by the following concrete case class:
package Models
case class PersonRecord(name: String) extends RecordObject
I then try to get a JSON string using some code like the following:
import io.circe.syntax._
import io.circe.generic.auto._
import org.http4s.circe._
// ...
val person = new PersonRecord(name = "Bob")
println(person._id, person.name) // prints some UUID and "Bob"
println(person.asJso) // {"name": "Bob"} -- what happened to "_id"?
As you can see, the property _id: String inherited from RecordObject is missing. I would expect that the built-in Encoder should function just fine for this use case. Do I really need to build my own?
Let's see what happens in encoder generation. Circe uses shapeless to derive its codecs, so its enough to check what shapeless resolves into to answer your question. So in ammonite:
# abstract class RecordObject {
val _id: String = java.util.UUID.randomUUID.toString
}
defined class RecordObject
# case class PersonRecord(name: String) extends RecordObject
defined class PersonRecord
# import $ivy.`com.chuusai::shapeless:2.3.3`, shapeless._
import $ivy.$ , shapeless._
# Generic[PersonRecord]
res3: Generic[PersonRecord]{type Repr = String :: shapeless.HNil} = ammonite.$sess.cmd3$anon$macro$2$1#1123d461
OK, so its String :: HNil. Fair enough - what shapeless does is extracting all fields available in constructor transforming one way, and putting all fields back through constructor if converting the other.
Basically all typeclass derivation works this way, so you should make it possible to pass _id as constructor:
abstract class RecordObject {
val _id: String
}
case class PersonRecord(
name: String,
_id: String = BSONObjectID.generate().stringify
) extends RecordObject
That would help type class derivation do its work. If you cannot change how PersonRecord looks like... then yes you have to write your own codec. Though I doubt it would be easy as you made _id immutable and impossible to set from outside through a constructor, so it would also be hard to implement using any other way.
I have a basic model with a case class
case class Record( id: Option[String],
data: Double,
user: String,
)
object RecordJsonFormats {
import play.api.libs.json.Json
implicit val recordFormat = Json.format[Record]
}
Field user is actually an ObjectId of other module also id is also an ObjectId yet then try to change String type to BSONObjectId macros in play.api.libs.json.Json break... so both user and if saved with object id fields get saved as String not ObjectId.
What is the optimal way to operate with ObjectIds in Play framework?
Maybe I should extend play.api.libs.json.Json with BSONObjectId?
Maybe there is a way to link models and IDs are tracked automatically without a need to declare them in model?
You can override the default type of _id. You just need to specify the type you want in the case class.
import java.util.UUID
import play.api.libs.json._
case class Record (_id: UUID = UUID.randomUUID())
object Record {
implicit val entityFormat = Json.format[Record]
}
MongoDB has a default _id field of type ObjectId, which uniquely identifies a document in a given collection. However, this _id typically does not have a semantic meaning in the context of the application domain. Therefore, a good practice is to introduce an additional id field as index of documents. This id can simply a Long number, no more or less.
Then, you can search documents by id easily, and do not care much about ObjectId.
This, https://github.com/luongbalinh/play-mongo/, is a sample project using Play 2.4.x and ReactiveMongo. Hopefully, it helps you.
For those using Official Mongo Scala Driver and Play Framework 2.6+, Here's my solution: https://gist.github.com/ntbrock/556a1add78dc287b0cf7e0ce45c743c1
import org.mongodb.scala.bson.ObjectId
import play.api.libs.json._
import scala.util.Try
object ObjectIdFormatJsonMacro extends Format[ObjectId] {
def writes(objectId: ObjectId): JsValue = JsString(objectId.toString)
def reads(json: JsValue): JsResult[ObjectId] = json match {
case JsString(x) => {
val maybeOID: Try[ObjectId] = Try{new ObjectId(x)}
if(maybeOID.isSuccess) JsSuccess(maybeOID.get) else {
JsError("Expected ObjectId as JsString")
}
}
case _ => JsError("Expected ObjectId as JsString")
}
}
Use it like this in your business objects:
case class BusinessTime(_id: ObjectId = new ObjectId(), payRate: Double)
object BusinessTime {
implicit val objectIdFormat = ObjectIdFormatJsonMacro
implicit val businessTimeFormat = Json.format[BusinessTime]
}
Here is spray-json example. Here is NullOptions trait.
The problem is when I declare a case class say
object MyJsonProtocol extends DefaultJsonProtocol {
implicit val some: RootJsonFormat[Some] = jsonFormat2(Some)
}
case class Some (
name:String,
age:Int
)
and json do not contains a field for example:
{
"name":"John"
}
I get: java.util.NoSuchElementException: key not found: age
So I have to add an Option and NullOption trait like that:
object MyJsonProtocol extends DefaultJsonProtocol with NullOptions {
implicit val some: RootJsonFormat[Some] = jsonFormat2(Some)
}
case class Some (
name:String,
age:Option[Int]
)
Everything works. But I do not want to have a case classes where all member are Option. Is there a way to configure spray json unmarshalling to just set nulls without additional Option type?
P.S.
I understand that in general Option is better then null check, but in my case it is just monkey code.
Also complete example of marshalling during response processing is here
The only way I can think of is to implement your own Protocol via read/write, which might be cumbersome. Below is a simplified example. Note that I changed the age to be an Integer instead of an Int since Int is an AnyVal, which is not nullable by default. Furthermore, I only consider the age field to be nullable, so you might need to adopt as necessary. Hope it helps.
case class Foo (name:String, age: Integer)
object MyJsonProtocol extends DefaultJsonProtocol {
implicit object FooJsonFormat extends RootJsonFormat[Foo] {
def write(foo: Foo) =
JsObject("name" -> JsString(foo.name),
"age" -> Option(foo.age).map(JsNumber(_)).getOrElse(JsNull))
def read(value: JsValue) = value match {
case JsObject(fields) =>
val ageOpt: Option[Integer] = fields.get("age").map(_.toString().toInt) // implicit conversion from Int to Integer
val age: Integer = ageOpt.orNull[Integer]
Foo(fields.get("name").get.toString(), age)
case _ => deserializationError("Foo expected")
}
}
}
import MyJsonProtocol._
import spray.json._
val json = """{ "name": "Meh" }""".parseJson
println(json.convertTo[Foo]) // prints Foo("Meh",null)
It seems you're out of luck
From the doc you linked:
spray-json will always read missing optional members as well as null optional members as None
You can customize the json writing, but not the reading.
I've a bunch of case classes that I use to build a complex object Publisher.
sealed case class Status(status: String)
trait Running extends Status
trait Stopped extends Status
case class History(keywords: List[String], updatedAt: Option[DateTime])
case class Creds(user: String, secret: String)
case class Publisher(id: Option[BSONObjectID], name: String, creds: Creds, status: Status, prefs: List[String], updatedAt: Option[DateTime])
I want to convert the Publisher into a JSON string using the play JSON API.
I used Json.toJson(publisher) and it complained about not having an implicit for Publisher. The error went away after I provided the following
implicit val pubWrites = Json.writes[Publisher]
As excepted it is now complaining about not being able to find implicits for Status, BSONObjectID and Creds. However, when I provide implicits for each Status and Creds it still complains.
implicit val statusWrites = Json.writes[Status]
implicit val credsWrites = Json.writes[Creds]
Any idea how to resolve this ? This is the first time I'm using Play JSON. I've used Json4s before and would like to try this using Play JSON if possible before I move go Json4s unless there are clear benefits for using/not using Json4s vs Play JSON.
The order of implicits are also important. From the least important to the most important. If Writes of Publisher requires Writes of Status, the implicit for Writes of Status should be before the Writes of Publisher.
Here is the code I tested to work
import play.modules.reactivemongo.json.BSONFormats._
import play.api.libs.json._
import reactivemongo.bson._
import org.joda.time.DateTime
sealed case class Status(status: String)
trait Running extends Status
trait Stopped extends Status
case class History(keywords: List[String], updatedAt: Option[DateTime])
case class Creds(user: String, secret: String)
case class Publisher(id: Option[BSONObjectID], name: String, creds: Creds, status: Status, prefs: List[String], updatedAt: Option[DateTime])
implicit val statusWrites = Json.writes[Status]
implicit val credsWrites = Json.writes[Creds]
implicit val pubWrites = Json.writes[Publisher]
val p = Publisher(
Some(new BSONObjectID("123")),
"foo",
Creds("bar","foo"),
new Status("foo"),
List("1","2"),
Some(new DateTime))
Json.toJson(p)
//res0: play.api.libs.json.JsValue = {"id":{"$oid":"12"},"name":"foo","creds":{"user":"bar","secret":"foo"},"status":{"status":"foo"},"prefs":["1","2"],"updatedAt":1401787836305}