reactivemongo BSON writer / reader with arbitrary JSON branch (also using spray) - mongodb

I am building REST API using spray. All is working well except this case class:
case class User(name: String, places: List[String], data: List[JsObject])
The key issue here is the data parameter. It contains a json object with arbitrary number of members, types, and levels - but still valid json.
Using spray, I am able to serialize/deserialize a request/response properly using:
object UserProtocol extends DefaultJsonProtocol {
implicit val userResonseFormat = jsonFormat3(User)
}
// ...
import demo.UserProtocol._
post {
path("users") {
entity(as[User]) { user: User =>
complete(user)
}
}
}
The problem is reading and writing BSON for reactivemongo. I cannot seem to figure out how to complete these:
implicit object UserWriter extends BSONDocumentWriter[User] {
def write(user: User): BSONDocument = BSONDocument(
"name" -> user.name,
"places" -> user.places,
"data" -> ???
}
implicit object UserReader extends BSONDocumentReader[User] {
def read(doc: BSONDocument): User = {
User(
doc.getAs[String]("name").get,
doc.getAs[List[String]]("places").get,
???
}
}
In the places of ???, How can I get this arbitrary JSON branch to serialize/deserialize BSON properly for reactivemongo?

This is a simple example that illustrate how to define readers and writers for a model. Hope it helps.
https://github.com/luongbalinh/play-mongo/blob/master/app/models/User.scala

Related

How to create an endpoint with Tapir in Scala with multiple Schemas

I’m just heading an issue when I’m trying to create an endpoint with multiple bodies shape.
My model looks like this:
sealed trait FileExampleTrait {
def kind: String
}
case class FileExampleOne(name: String, length: Int) extends FileExampleTrait {
override def kind: String = “one”
}
case class FileExampleTwo(name: String) extends FileExampleTrait {
override def kind: String = “two”
}
case class FileExampleResponse(message: String)
And I’m trying to create this endpoint:
val fileExample = baseEndpoint.post
.in(“example”)
.in(jsonBody[FileExampleTrait])
.out(jsonBody[FileExampleResponse])
.summary(“something”)
.description(“something”)
The implementation of the endpoint looks like this:
private val fileExample = toAkkaRoute(jwtConsumer, errorHandler)(
FileApi.fileExample, { (scope: RequestScope, input: (FileExampleTrait)) =>
print(scope)
input match {
case FileExampleOne(name, _) => Future.successful(FileExampleResponse(name).asRight)
case FileExampleTwo(name) => Future.successful(FileExampleResponse(name).asRight)
}
}
)
This is just an example on what I’m trying to create. I added the schema derivation based on this:
val sOne = Schema.derived[FileExampleOne]
val sTwo = Schema.derived[FileExampleTwo]
implicit val sExampleTrait: Schema[FileExampleTrait] =
Schema.oneOfUsingField[FileExampleTrait, String](_.kind, _.toString)(“one” -> sOne, “two” -> sTwo)
I created a test for trying the endpoint based on Akka HTTP:
test(“Example test”) {
new Fixture() {
val request = FileExampleOne(“name”, 1)
Post(s”/api/v1/files/example”, jsonEntity(request)).withHeaders(requestHeaders) ~> wrappedRoute ~> check {
response should be(successful)
contentType shouldEqual ContentTypes.`application/json`
}
}
}
The error I’m getting is the following:
Response error: {“code”:400,“message”:“Invalid value for: body (No constructor for type FileExampleTrait, JObject(List((name,JString(name)), (length,JInt(1)))))“}
I was following this documentation.
Well that's because a trait doesn't have a constructor as indicated in the error itself. I think I see where you're going, you want to try parsing the body as one of the traits subclasses. So imagine you have this type/class hierarchy:
T // parent trait
/ \
C1 C2 // class 1, etc...
/
C3
Now you want to deserialize some JSON into trait T, you need to define your custom behavior, like "First try converting into C3, if failed, try converting to C2, if failed again, try converting to C1", and you'll get your T value. Now depending on the JSON library you use, the implementation might differ, see the documentation by softwaremill to get more information about how to deal with JSONs in tapir, and if you use Play Json, I can recommend:
object FileExampleOne {
implicit val reader: Reads[FileExampleOne] = Json.reads
}
object FileExampleTwo {
implicit val reader: Reads[FileExampleTwo] = Json.reads
}
object FileExampleTrait {
implicit val reads: Reads[FileExampleTrait] = json => {
json.validate[FileExampleOne] orElse json.validate[FileExampleTwo]
}
}
You can see it running on scastie. And based on the tapir documentations, you need a Codec for your types, and one of the approaches to create your coded for JSON, is using one of tapir's supported libraries (circe, Play Json, ...).

elastic4s: deserializing search results

I'm using elastic4s library to query elasticsearch (ES). Version of elastic4s and ES itself 2.4.0.
Suppose I have a compound object that I put to ES like
case class MyObject(id: Long, vall: KeyVal, vals: Seq[KeyVal])
where KeyVal is
case class KeyVal(id: Long, name: String)
Now I queried ES and got the response which I want to deserialiize back to MyObject:
implicit object MyObjectHitAs extends HitAs[MyObject] {
override def as(hit: RichSearchHit): MyObject = {
MyObject(
hit.field("id").getValue[String]
KeyVal(hit.field("vall.id").getValue[Long], field("vall.name").getValue[String]),
//what should I code here to get the Seq[KeyVal] ???
)
}
}
Please explain how can I deserialize the Array of KeyVal. Thank you.
In the more recent versions of elastic4s, ie 5.0 onwards, you would use the HitReader typeclass. Your example would then look like this.
implicit object MyObjectHitAs extends HitReader[MyObject] {
override def read(hit: Hit): Either[Throwable, MyObject] = {
val obj = MyObject(
hit.sourceField("id").toString.toLong,
KeyVal(hit.sourceField("vall.id").toString.toLong, hit.sourceField("vall.name").toString),
hit.sourceField("vals").asInstanceOf[Seq[AnyRef]].map { entry =>
KeyVal(hit.sourceField("vall.id").toString.toLong, hit.sourceField("vall.name").toString)
}
)
Right(obj)
}
}
Although it is a lot easier to use the built in json mappers than hand craft it.

How can I parse a json and extract to different case classes depending of its content

I'm trying to parse different types of events in json format using json4s. I have written some case classes to represent the different events all inheriting from a base class Event:
abstract class Event{ def EventType : String }
case class StartSession(val EventType: String, val Platform: String) extends Event
case class AdView(val EventType: String, val EventSubtype: String) extends Event
This is the function I use to parse the StartSession event:
def parser(json: String): Event = {
val parsedJson = parse(json)
val s = parsedJson.extract[StartSession]
return s
}
This function will correctly parse a json like {"EventType":"StartSession","Platform":"Portal"}
I am looking for a way to generalize the parser function so I can use it to parse all types of events that inherits from Event and then do pattern matching over the return value of the function.
Type hints provide the solution to the problem. If you have a polymorphic type, such as Event, the type hint (here EventType) tells json4s into which actual type a json object should be deserialized.
For reference, have a look at the github page of json4s. There is a section called "Serializing polymorphic Lists".
Since the type hint is only necessary for deserialization, we can get rid of the field EventType in Event.
abstract class Event
case class StartSession(Platform: String) extends Event
case class AdView(EventSubtype: String) extends Event
We need to bring a Formats instance into the scope of extract. The instance tells json4s how to perform deserialization. In our case, we need to specialize typeHintFieldName and typeHints. The first is the key of our type hint, in the example it is "EventType". The latter is a mapping from string values to classes. If we are using the class name as value, then ShortTypeHints will do the trick. Othwerise, we could implement our own, specialized TypeHints.
A concrete solution could look as follows:
def main(args: Array[String]): Unit ={
val json =
s"""
|[
| {
| "EventType": "StartSession",
| "Platform": "Portal"
| },
| {
| "EventType": "AdView",
| "EventSubtype": "SpecializedView"
| }
|]
""".stripMargin
implicit val formats = new DefaultFormats {
override val typeHintFieldName: String = "EventType"
override val typeHints: TypeHints =
ShortTypeHints(
List(classOf[StartSession], classOf[AdView])
)
}
val parsedJson = parse(json)
val s = parsedJson.extract[List[Event]]
println(s)
}

spray-json for normal classes (non case) on a List

I'm finding myself in a situation in which I need to serialize into JSON a non case class.
Having a class as:
class MyClass(val name: String) {
def SaySomething() : String = {
return "Saying something... "
}
}
I've created a JsonProtocol for this class:
object MyClassJsonProtocol extends DefaultJsonProtocol {
implicit object MyClassJsonFormat extends JsonWriter[MyClass] {
override def write(obj: MyClass): JsValue =
JsObject(
"name" -> JsString(obj.name)
)
}
}
Later on in the code I import the protocol..
val aListOfMyClasses = List[MyClass]() ... // lets assume that has items and not an empty list
import spray.json._
import MyClassJsonProtocol._
val json = aListOfMyClasses.toJson
When trying to build the project I get the following error:
Cannot find JsonWriter or JsonFormat for type class List[MyClass]
spray-json has already a format for generic list and I'm providing a format for my class, what would be the problem?
Thanks in advance...!!!
When I extended MyClassJsonFormat from JsonFormat instead of JsonWriter, it stared working fine. Looks like the CollectionFormats trait will work only if you extend from JsonFormat
The following code compiles fine for me
object MyClassJsonProtocol extends DefaultJsonProtocol {
implicit object MyClassJsonFormat extends JsonFormat[MyClass] {
override def write(obj: MyClass): JsValue =
JsObject(
"name" -> JsString(obj.name)
)
override def read(json: JsValue): MyClass = new MyClass(json.convertTo[String])
}
}
The reason seems to be mentioned here:
An issue you might run into with just JsonReader/JsonWriter is that
when you try to lookup JsonReader/JsonWriter for Option or a
collection, it looks for a JsonFormat for the contained type, which
will fail. Not sure if there is something I am missing that will fix
that issue.
You and I have run into this. I don't see other way out at the moment than #user007's suggestion to use a full JsonFormat. That, itself, brings more difficulties at least to me - I was planning to use the default reader for my class.
Oh, well...

Play2 + Casbah: How to provide an implicit Writes for ObjectId

there is a simple model class that contains some database ids. It looks like this:
case class Post(id: ObjectId, owner: Option[ObjectId], title: String)
object Post {
implicit val implicitPostWrites = Json.writes[Post]
}
With this code, the compiler gives me the following error:
No implicit Writes for com.mongodb.casbah.commons.TypeImports.ObjectId available.
implicit val implicitFooWrites = Json.writes[Foo]
It is obvious what's missing, but I don't know how to provide an implicit Writes for com.mongodb.casbah.commons.TypeImports.ObjectId. How can this be done?
The error means that it doesn't know how to serialize ObjectId and expects you to provide a Writer for it. This is one way to serialize it:
object Post {
implicit val objectIdWrites = new Writes[ObjectId] {
def writes(oId: ObjectId): JsValue = {
JsString(oId.toString)
}
}
implicit val implicitPostWrites = Json.writes[Post]
}
More information and explanations are available here.