I'm trying to store a nested document in MongoDB through Scala. The document looks like:
Project {
"_id": ObjectId("528547370cf6e41449003512"),
"highLevelCode": NumberLong(3),
"description": [
{"_id": ObjectId("528547370cf6e41449003521"),
"lang": "en",
"desc": "desc in English"},
{"_id ": ObjectId("528547370cf6e41449003522"),
"lang": "fr",
"desc": "desc en francais"}],
"budget": NumberLong(12345)
}
Basically I want to store nested descriptions, which could be of multiple languages in the Project document.
The code I wrote is:
import reactivemongo.bson._
import reactivemongo.bson.handlers.{BSONWriter, BSONReader}
import reactivemongo.bson.BSONLong
import reactivemongo.bson.BSONString
case class LocaleText(
id: Option[BSONObjectID],
lang: String,
textDesc: String
)
object LocaleText {
implicit object LocaleTextBSONReader extends BSONReader[LocaleText] {
def fromBSON(document: BSONDocument): LocaleText = {
val doc = document.toTraversable
LocaleText(
doc.getAs[BSONObjectID]("_id"),
doc.getAs[BSONString]("lang").map(_.value).get,
doc.getAs[BSONString]("textDesc").map(_.value).get
)
}
}
implicit object LocaleTextBSONWriter extends BSONWriter[LocaleText] {
def toBSON(localText: LocaleText) = {
BSONDocument(
"_id" -> localText.id.getOrElse(BSONObjectID.generate),
"lang" -> BSONString(localText.lang),
"textDesc" -> BSONString(localText.textDesc)
)
}
}
}
case class Project(
id: Option[BSONObjectID],
description: List[LocaleText],
budget: Option[Long]
)
object Project {
implicit object ProjectReader extends BSONReader[Project]{
def fromBSON(doc: BSONDocument): Project = {
val document = doc.toTraversable
Project(
document.getAs[BSONObjectID]("_id"),
document.getAs[BSONArray]("description").map { values =>
values.values.toList.flatMap { case value =>
value match {
case v: LocaleText => Some(v.asInstanceOf[LocaleText])
case _ => None
}
}
}.getOrElse(List.empty),
document.getAs[BSONLong]("budget").map(_.value)
)
}
}
implicit object ProjectWriter extends BSONWriter[Project]{
def toBSON(project: Project): BSONDocument = {
BSONDocument(
"_id" -> project.id.getOrElse(BSONObjectID.generate),
"description" -> BSONArray(project.description)
).append(Seq(
project.budget.map(b => "budget" -> BSONLong(b))
).flatten:_*)
}
}
}
However, it gave me compilation error like
overloaded method value apply with alternatives: [error] (producer: reactivemongo.bson.Implicits.Producer[(String, reactivemongo.bson.BSONValue)],producers: reactivemongo.bson.Implicits.Producer[(String, reactivemongo.bson.BSONValue)])reactivemongo.bson.AppendableBSONDocument
[error] (els: (String, reactivemongo.bson.BSONValue))reactivemongo.bson.AppendableBSONDocument
[error] cannot be applied to ((String, reactivemongo.bson.BSONObjectID), List[LocaleText])...
Basically Scala doesn't like the line
"description" -> BSONArray(project.description)
However, the following alternative works although I cannot use a List/Array to allow more than two languages:
case class LocaleText(
enDesc: String,
frDesc: String)
case class Project(
id: Option[BSONObjectID],
description: LocaleText)
object Project {
implicit object LocaleTextBSONReader extends BSONReader[LocaleText] {
def fromBSON(document: BSONDocument): LocaleText = {
val doc = document.toTraversable
LocaleText(
doc.getAs[BSONString]("enDesc").map(_.value).get,
doc.getAs[BSONString]("frDesc").map(_.value).get
)
}
}
implicit object LocaleTextBSONWriter extends BSONWriter[LocaleText] {
def toBSON(localText: LocaleText) = {
BSONDocument(
"enDesc" -> BSONString(localText.enDesc),
"frDesc" -> BSONString(localText.frDesc)
)
}
}
implicit object ProjectReader extends BSONReader[Project]{
def fromBSON(doc: BSONDocument): Project = {
val document = doc.toTraversable
Project(
document.getAs[BSONObjectID]("_id"),
document.getAs[BSONString]("iatiId").map(_.value).get,
LocaleTextBSONReader.fromBSON(document.getAs[BSONDocument]("description").get)
}
}
implicit object ProjectWriter extends BSONWriter[Project]{
def toBSON(project: Project): BSONDocument = {
BSONDocument(
"_id" -> project.id.getOrElse(BSONObjectID.generate),
"iatiId" -> BSONString(project.iatiId),
"description" -> LocaleTextBSONWriter.toBSON(project.description)
}
}
How can I convert project.description, which a List of LocaleText to BSONArray for Mongo? I appreciate if you can shed some light on my problem. Thank you very much for your help.
Finally I found the solution to my own question, hope this will help some others who struggle with ReactiveMongo 0.8 as well:
case class LocaleText(
lang: String,
desc: String)
case class Project(
id: Option[BSONObjectID],
descriptions: List[LocaleText])
object Project {
implicit object LocaleTextBSONReader extends BSONReader[LocaleText] {
def fromBSON(document: BSONDocument): LocaleText = {
val doc = document.toTraversable
LocaleText(
doc.getAs[BSONString]("lang").get.value,
doc.getAs[BSONString]("desc").get.value
)
}
}
implicit object LocaleTextBSONWriter extends BSONWriter[LocaleText] {
def toBSON(localText: LocaleText) = {
BSONDocument(
"lang" -> BSONString(localText.lang),
"desc" -> BSONString(localText.desc)
)
}
}
implicit object ProjectReader extends BSONReader[Project]{
def fromBSON(doc: BSONDocument): Project = {
val document = doc.toTraversable
Project(
document.getAs[BSONObjectID]("_id"),
document.getAs[BSONArray]("descriptions").get.toTraversable.toList.map { descText =>
LocaleTextBSONReader.fromBSON(descText.asInstanceOf[TraversableBSONDocument]
}
)
}
}
implicit object ProjectWriter extends BSONWriter[Project]{
def toBSON(project: Project): BSONDocument = {
BSONDocument(
"_id" -> project.id.getOrElse(BSONObjectID.generate),
"descriptions" -> BSONArray(project.descriptions.map {
description => LocaleTextBSONWriter.toBSON(description)
}: _*)
}
}
It might be an issue in the library. I tested your code using the latest version of reactivemongo and it compiled just fine (I needed to adapt your code to fit the new syntax for BSONReaders and BSONWriters but that shouldn't have any influence on the error).
Using reactivemongo 0.10.0 you can even use the newly provided macros:
import reactivemongo.bson._
case class LocaleText(id: Option[BSONObjectID], lang: String, textDesc: String)
object LocaleText {
implicit val localeTextBSONHandler = Macros.handler[LocaleText]
}
case class Project(id: Option[BSONObjectID], description: List[LocaleText], budget: Option[Long])
object Project {
implicit val projectBSONHandler = Macros.handler[Project]
}
Related
Serialization works fine but I have nothing for deserialization. I found interesting solution for abstract class here How to serialize sealed abstract class with Json4s in Scala? but it doesn't deal with trees.
This the code of my test with a standard JSON4S :
import org.json4s._
import org.json4s.native.JsonMethods._
import org.json4s.native.Serialization.{ read, write }
import org.json4s.native.Serialization
abstract class Tree
case class Node(nameN: String, trees: List[Tree]) extends Tree
case class Leaf(nameL: String) extends Tree
object Tree extends App {
implicit val formats = Serialization.formats(NoTypeHints)
// object creation to test the serialization
val root =
Node(
"Grand Pavois project",
List(
Node(
"studies",
List(
Leaf("preliminary studies"),
Leaf("detailled studies")
)
),
Node(
"realization",
List(
Leaf("ground"),
Leaf("building"),
Leaf("roof")
)
),
Node(
"delivery",
List(
Leaf("quality inspection"),
Leaf("customer delivery")
)
)
)
)
val serialized = write(root) // object creation and serialization
println(s"serialized: $serialized") // print the result, this is OK
// and now what about deserialization?
// string creation for deserialization
// ( it is the same as serialized above, I do like that to trace for the demo)
val rootString = """
{
"nameN": "Grand Pavois project",
"trees": [
{
"nameN": "studies",
"trees": [
{
"nameL": "preliminary studies"
},
{
"nameL": "detailled studies"
}
]
},
{
"nameN": "realization",
"trees": [
{
"nameL": "ground"
},
{
"nameL": "building"
},
{
"nameL": "roof"
}
]
},
{
"nameN": "delivery",
"trees": [
{
"nameL": "quality inspection"
},
{
"nameL": "customer delivery"
}
]
}
]
}
"""
//standard deserialization below that produce an error :
// "Parsed JSON values do not match with class constructor"
val rootFromString = read[Tree](rootString)
}
Now I guess the solution is with a custom deserializer probably a recusive one but how to define it? That is the question. Thanks for your help.
This solution doesn't use a custom deserializer, but instead creates a type that matches both Node and Leaf and then converts to the appropriate type later.
case class JsTree(nameN: Option[String], nameL: Option[String], trees: Option[List[JsTree]])
def toTree(node: JsTree): Tree = node match {
case JsTree(Some(name), None, Some(trees)) =>
Node(name, trees.map(toTree))
case JsTree(None, Some(name), None) =>
Leaf(name)
case _ =>
throw new IllegalArgumentException
}
val rootFromString = toTree(read[JsTree](rootString))
The JsTree class will match both Node and Leaf values because it has option fields that match all the fields in both classes. The toTree method recursively converts the JsTree to the appropriate Tree subclass based on which fields are actually present.
Update: Custom serializer
Here is the solution using a custom serializer:
import org.json4s.JsonDSL._
class TreeSerializer extends CustomSerializer[Tree](format => ({
case obj: JObject =>
implicit val formats: Formats = format
if ((obj \ "trees") == JNothing) {
Leaf(
(obj \ "nameL").extract[String]
)
} else {
Node(
(obj \ "nameN").extract[String],
(obj \ "trees").extract[List[Tree]]
)
}
}, {
case node: Node =>
JObject("nameN" -> JString(node.nameN), "trees" -> node.trees.map(Extraction.decompose))
case leaf: Leaf =>
"nameL" -> leaf.nameL
}))
Use it like this:
implicit val formats: Formats = DefaultFormats + new TreeSerializer
read[Tree](rootString)
{
"cars": [{
"amount": 120.00,
"name": "Car1"
}, {
"amount": 245.00,
"name": "Car2"
}]
}
I am reading above JSON as following in my Controller
val body: JsObject = request.body.asInstanceOf[JsObject]
I am having following CASE CLASS
case class BIC(name: String, amount: Double)
I want to create List[BIC] objects by reading data from JSON [e.g. body] using Functional style
Use Play JSON.
Example:
case class Wrapper(cars: List[Bic])
case class BIC(name: String, amount: Double)
Then in your controller:
implicit val wrapperFormats = Json.format[Wrapper]
implicit val bICFormats = Json.format[BIC]
def postCars(): Action[JsValue] = Action(json.parse) { implicit request =>
request.body.validate[Wrapper] match {
case JsSuccess(obj, _) => {
//do something with obj.
}
case JsError(err) => {
BadRequest(
JsObject(
"error" -> err.toString
)
)
}
}
}
Please note that I am returning Action[JsValue] this is so JQuery will run success when using AJAX.
I hope this helps,
Rhys
another reference:
https://www.playframework.com/documentation/2.5.x/ScalaJsonCombinators
First, define two case classes for your model like this :
object Models {
case class Bic(name : String, amount : Double)
object Bic {
implicit val BicFormat = Json.format[Bic]
}
case class Cars(bics : List[Bic])
object Cars {
implicit val CarsFormat = Json.format[Cars]
}
}
You're using the Play Framework so you can use the JSON library.
In your controller, if you want to read the bics, you can do it like that :
def getCars = Action(parse.json) { request =>
request.body.validate[Cars] map { cars =>
// treat your cars ..
}
}
Trying to convert class to Json.
Here is my class, it includes other two classes:
case class GoodEdit(good: Good, data: List[(String, Option[GoodText])])
case class Good(
id: Long,
partnumber: Option[String] = None
)
case class GoodText(
goodid: Long,
languageid: Long,
title: String,
description: Option[String] = None)
And here are my writers:
object GoodWriters {
implicit val goodWrites = new Writes[Good] {
def writes(good: Good) = Json.obj(
"id" -> good.id,
"partnumber" -> good.partnumber
)
}
implicit val goodTextWrites = new Writes[GoodText] {
def writes(goodText: GoodText) = Json.obj(
"goodid" -> goodText.goodid,
"languageid" -> goodText.languageid,
"title" -> goodText.title,
"description" -> goodText.description
)
}
implicit val GoodEditWrites = new Writes[GoodEdit] {
def writes(goodEdit: GoodEdit) = Json.obj(
"good" -> Json.toJson(goodEdit.good),
"data" -> Json.toJson(
for ((lang, goodTextOpt) <- goodEdit.data ) yield Json.obj(lang -> goodTextOpt)
)
)
}
Then, in controller I try it to use like this:
Action {
import jwriters.GoodWriters._
GoodEditAggregate.get(id).map{
a => Ok(Json.toJson(a))
}.getOrElse(Ok(Json.toJson(Json.obj("status" -> "error","message" -> "Can't find good with this id"))))
}
And compilator complaining on this part: Ok(Json.toJson(a))
No Json serializer found for type GoodEdit. Try to implement an
implicit Writes or Format for this type
Can't understand what is wrong. I've imported writers for objects already
Try to Import Goodwrites globally
Given the following BSONDocument...
val values = BSONDocument("values" -> BSONArray("one", "two", "three"))
How do I convert it to a List? I've tried this...
values.getAs[List[String]]("values").getOrElse(List.empty))
... but it doesn't work - I always get List.empty.
Am I missing something?
EDIT
OK... I think it is worth describing the real case. I ran the distinct command and this was the result:
values: {
values: [
0: BSONObjectID("55d0f641a100000401b7e454")
],
stats: {
n: BSONInteger(1),
nscanned: BSONInteger(1),
nscannedObjects: BSONInteger(1),
timems: BSONInteger(0),
cursor: BSONString(BtreeCursor projectId_1)
},
ok: BSONDouble(1.0)
}
I need to transform values to a Scala List[String] like this:
List("55d0f641a100000401b7e454")
Here is my solution. First, I've defined a BSONReader[BSONValue, String] like this...
package object bsonFormatters {
implicit object BSONValueStringReader extends BSONReader[BSONValue, String] {
def read(bson: BSONValue) = bson match {
case oid: BSONObjectID => oid.stringify
}
}
... and then just imported it in my companion object like this:
import reactivemongo.bson.{BSONString, BSONDocument}
import reactivemongo.core.commands.{CommandError, BSONCommandResultMaker, Command}
case class Distinct(
collectionName: String,
field: String,
query: Option[BSONDocument] = None
) extends Command[Seq[String]] {
override def makeDocuments = BSONDocument(
"distinct" -> BSONString(collectionName),
"key" -> field,
"query" -> query
)
val ResultMaker = Distinct
}
object Distinct extends BSONCommandResultMaker[Seq[String]] {
import bsonFormatters._ // this makes the trick
def apply(document: BSONDocument) = CommandError.checkOk(
document,
Some("distinct")
).toLeft(
document.getAs[List[String]]("values").getOrElse(List.empty)
)
}
I hope it helps.
I'm trying to write readers/writers for my case classes after reading:
https://github.com/sgodbillon/reactivemongo-demo-app/blob/master/app/models/articles.scala
https://github.com/zenexity/ReactiveMongo/blob/master/driver/samples/BSON.scala
but I'm having trouble in making it work.
I have a leadCategory that can consist of several word objects.
package models
import org.joda.time.DateTime
import reactivemongo.bson._
import reactivemongo.bson.handlers.{BSONWriter, BSONReader}
import reactivemongo.bson.BSONDateTime
import reactivemongo.bson.BSONString
LeadCategory:
case class LeadCategory(
id: Option[BSONObjectID],
categoryId: Long,
categoryName: String,
creationDate: Option[DateTime],
updateDate: Option[DateTime],
words: List[Word]
)
object LeadCategory {
implicit object LeadCategoryBSONReader extends BSONReader[LeadCategory] {
def fromBSON(document: BSONDocument): LeadCategory = {
val doc = document.toTraversable
LeadCategory(
doc.getAs[BSONObjectID]("_id"),
doc.getAs[BSONLong]("caregoryId").get.value,
doc.getAs[BSONString]("categoryName").get.value,
doc.getAs[BSONDateTime]("creationDate").map(dt => new DateTime(dt.value)),
doc.getAs[BSONDateTime]("updateDate").map(dt => new DateTime(dt.value)),
doc.getAs[List[Word]]("words").toList.flatten)
}
}
implicit object LeadCategoryBSONWriter extends BSONWriter[LeadCategory] {
def toBSON(leadCategory: LeadCategory) = {
BSONDocument(
"_id" -> leadCategory.id.getOrElse(BSONObjectID.generate),
"caregoryId" -> BSONLong(leadCategory.categoryId),
"categoryName" -> BSONString(leadCategory.categoryName),
"creationDate" -> leadCategory.creationDate.map(date => BSONDateTime(date.getMillis)),
"updateDate" -> leadCategory.updateDate.map(date => BSONDateTime(date.getMillis)),
"words" -> leadCategory.words)
}
}
Word:
case class Word(id: Option[BSONObjectID], word: String)
object Word {
implicit object WordBSONReader extends BSONReader[Word] {
def fromBSON(document: BSONDocument): Word = {
val doc = document.toTraversable
Word(
doc.getAs[BSONObjectID]("_id"),
doc.getAs[BSONString]("word").get.value
)
}
}
implicit object WordBSONWriter extends BSONWriter[Word] {
def toBSON(word: Word) = {
BSONDocument(
"_id" -> word.id.getOrElse(BSONObjectID.generate),
"word" -> BSONString(word.word)
)
}
}
}
I'm getting a compile error at:
"words" -> leadCategory.words)
stating:
Too many arguments for method apply(ChannelBuffer)
Type mismatch found: (String, List[Words]) required required implicits.Producer[(String, BsonValue)]
What have I missed? Maybe I have misunderstood the docs...
Try to declare the "Word" implicit objects above the "LeadCategory", if you are putting all of it in the same file.
Scala cannot find the implicit objects for the List[Word] - assuming you are using ReactiveMongo 0.9.
All of your types should a derivative of a BSON type. I think you want to use BSONArray instead of a List.