In order to do geospatial queries in MongoDB a document with a location (with a 2d or 2dsphere geospatial index) should look something like this:
{
_id: …,
loc: {
type: "Point",
coordinates: [ <longitude>, <latitude> ]
}
}
I'm very new to Scala, ReactiveMongo and Play Framework, but in my opinion an obvious way to use such a location is through a case class like:
case class Point(lon: Double, lat: Double)
And the JSON representation that a website's API deals with should look something like:
{
_id: …
loc: [ <longitude>, <latitude> ]
}
Now, I can't figure out how to tell my ReactiveMongo model to serialize/deserialize between these formats.
My controller looks like this:
package controllers
import play.api._
import play.api.mvc._
import play.api.libs.json._
import scala.concurrent.Future
// Reactive Mongo imports
import reactivemongo.api._
import scala.concurrent.ExecutionContext.Implicits.global
// Reactive Mongo plugin
import play.modules.reactivemongo.MongoController
import play.modules.reactivemongo.json.collection.JSONCollection
object Application extends Controller with MongoController {
def collection: JSONCollection = db.collection[JSONCollection]("test")
import play.api.data.Form
import models._
import models.JsonFormats._
def createCC = Action.async {
val user = User("John", "Smith", Point(-0.0015, 51.0015))
val futureResult = collection.insert(user)
futureResult.map(_ => Ok("Done!"))
}
}
I tried to use a PointWriter and PointReader. This is my models.scala:
package models
import reactivemongo.bson._
import play.modules.reactivemongo.json.BSONFormats._
case class User(
// _id: Option[BSONObjectID],
firstName: String,
lastName: String,
loc: Point)
case class Point(lon: Double, lat: Double)
object Point {
implicit object PointWriter extends BSONDocumentWriter[Point] {
def write(point: Point): BSONDocument = BSONDocument(
"type" -> "Point",
"coordinates" -> Seq(point.lat, point.lon)
)
}
implicit object PointReader extends BSONReader[BSONDocument, Point] {
def read(doc: BSONDocument): Point = Point(88, 88)
}
}
object JsonFormats {
import play.api.libs.json.Json
import play.api.data._
import play.api.data.Forms._
import play.api.libs.json._
import play.api.libs.functional.syntax._
implicit val pointFormat = Json.format[Point]
}
When I call the controller action createCC I would expect have a properly formatted Point object the newly created document, but what I actually get is something like:
{
"_id": ObjectId("52ac76dd1454bbf6d96ad1f1"),
"loc": {
"lon": -0.0015,
"lat": 51.0015
}
}
So my attempt to use PointWriter and PointReader to tell ReactiveMongo how to write such a Point object to the database has no effect at all.
Can anybody help me understand what I have to do?
(I come from a PHP background and try to get my head round Scala...)
Update: Thanks to tmbo's answer I came up with this writer:
val pointWrites = Writes[Point]( p =>
Json.obj(
"type" -> JsString("Point"),
"coordinates" -> Json.arr(JsNumber(p.lon), JsNumber(p.lat))
)
)
The problem you are facing is related to a mixup between
JSONCollectionandBSONCollection.BSONCollectionis the default collection reactivemongo uses. This implementation needs an implementation of aBSONDocumentWriterand aBSONReaderfor a case class to get (de-)serialised.JSONCollectionon the other hand is the default collection implementation that the play-reactive module uses. Since you defined the collection to be aJSONCollectionindb.collection[JSONCollection]("test")you need to provide an implicit json format.The json format you provide is
This will serialize an object to the following format
If you want to serialise your
Pointto an array you need to replace the above implicitpointFormat:You actualy don't need the
BSONReaderandBSONDocumentWriter.Edit: Here is a reads that also validates the
typeattribute of the document: