Question

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))
    )
)
Was it helpful?

Solution

The problem you are facing is related to a mixup between JSONCollectionand BSONCollection.

BSONCollection is the default collection reactivemongo uses. This implementation needs an implementation of a BSONDocumentWriterand a BSONReaderfor 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 a JSONCollection in db.collection[JSONCollection]("test") you need to provide an implicit json format.

The json format you provide is

implicit val pointFormat = Json.format[Point]

This will serialize an object to the following format

{
    "lon": -0.0015,
    "lat": 51.0015 
}

If you want to serialise your Point to an array you need to replace the above implicit pointFormat:

import play.api.libs.json._
import play.api.libs.json.Reads._

case class Point(lng: Double, lat: Double)

object Point {

  val pointWrites = Writes[Point]( p => Json.toJson(List(p.lng, p.lat)))

  val pointReads = minLength[List[Double]](2).map(l => Point(l(0), l(1)))

  implicit val pointFormat = Format(pointReads, pointWrites)
}

You actualy don't need the BSONReaderand BSONDocumentWriter.

Edit: Here is a reads that also validates the type attribute of the document:

val pointReads =
  (__ \ 'type).read[String](constraints.verifying[String](_ == "Point")) andKeep
    (__ \ 'coordinates).read[Point](minLength[List[Double]](2).map(l => Point(l(0), l(1))))
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top