I use ReactiveMongo 0.11.11 for Play 2.5 and want to convert a BSONDocument to a JsObject.
For most BSON data types (String, Int...) the defaults are perfectly fine to let the library do the job. For BSON type DateTime (BSONDateTime
) the value of the JSON property does not give me the format I need.
The JSON value for a Date is a JsObject with property name $date
and a UNIX timestamp in milliseconds as its value:
{
"something": {
"$date": 1462288846873
}
}
The JSON I want is a String representation of the Date like this:
{
"something": "2016-05-03T15:20:46.873Z"
}
Unfortunately I don't know how to override the default behaviour without rewriting everything or changing code in the library itself.
This is where I think it happens (source code):
val partialWrites: PartialFunction[BSONValue, JsValue] = {
case dt: BSONDateTime => Json.obj("$date" -> dt.value)
}
My version would have to look like this:
val partialWrites: PartialFunction[BSONValue, JsValue] = {
case dt: BSONDateTime =>
JsString(Instant.ofEpochMilli(dt.value).toString)
}
Is it possible to override this bit?
I have created an experiment...
import java.time.Instant
import play.api.libs.json._
import reactivemongo.bson._
import reactivemongo.play.json.BSONFormats.BSONDocumentFormat
object Experiment {
// Original document (usually coming from the database)
val bson = BSONDocument(
"something" -> BSONDateTime(1462288846873L) // equals "2016-05-03T15:20:46.873Z"
)
// Reader: converts from BSONDateTime to JsString
implicit object BSONDateTimeToJsStringReader extends BSONReader[BSONDateTime, JsString] {
def read(bsonDate: BSONDateTime): JsString = {
JsString(Instant.ofEpochMilli(bsonDate.value).toString)
}
}
// Reader: converts from BSONDateTime to JsValue
implicit object BSONDateTimeToJsValueReader extends BSONReader[BSONDateTime, JsValue] {
def read(bsonDate: BSONDateTime): JsValue = {
JsString(Instant.ofEpochMilli(bsonDate.value).toString)
}
}
// Read and print specific property "something" using the `BSONReader`s above
def printJsDate = {
val jsStr: JsString = bson.getAs[JsString]("something").get
println(jsStr) // "2016-05-03T15:20:46.873Z"
val jsVal: JsValue = bson.getAs[JsValue]("something").get
println(jsVal) // "2016-05-03T15:20:46.873Z"
}
// Use ReactiveMongo's default format to convert a BSONDocument into a JsObject
def printAsJsonDefault = {
val json: JsObject = BSONDocumentFormat.writes(bson).as[JsObject]
println(json) // {"something":{"$date":1462288846873}}
// What I want: {"something":"2016-05-03T15:20:46.873Z"}
}
}
I'd like to note that the BSONDateTime conversion to JsValue should always work when I convert a BSONDocument to JsObject, not only when I manually pick a specific known property. This means the property "something" in my example could have any name and also appear in a sub-document.
BTW: In case you wonder, I generally work with BSON collections in my Play project, but I don't think it makes a difference in this case anyway.
Edit
I've tried providing a Writes[BSONDateTime]
, but unfortunately it's not being used and I still get the same result as before. Code:
import java.time.Instant
import play.api.libs.json._
import reactivemongo.bson.{BSONDocument, BSONDateTime}
object MyImplicits {
implicit val dateWrites = Writes[BSONDateTime] (bsonDate =>
JsString(Instant.ofEpochMilli(bsonDate.value).toString)
)
// I've tried this too:
// implicit val dateWrites = new Writes[BSONDateTime] {
// def writes(bsonDate: BSONDateTime) = JsString(Instant.ofEpochMilli(bsonDate.value).toString)
// }
}
object Experiment {
// Original document (usually coming from the database)
val bson = BSONDocument("something" -> BSONDateTime(1462288846873L))
// Use ReactiveMongo's default format to convert a BSONDocument into a JsObject
def printAsJson = {
import reactivemongo.play.json.BSONFormats.BSONDocumentFormat
import MyImplicits.dateWrites // import is ignored
val json: JsObject = BSONDocumentFormat.writes(bson).as[JsObject]
//val json: JsValue = Json.toJson(bson) // I've tried this too
println(json) // {"something":{"$date":1462288846873}}
}
}
As for any type, the BSON value are converted to Play JSON using instances of Writes[T]
.
There you needs to provide in the implicit scope your own Writes[BSONDateTime]
.
import reactivemongo.bson._
import play.api.libs.json._
object MyImplicits {
implicit val dateWrites = Writes[BSONDateTime] { date =>
???
}
def jsonDoc(doc: BSONDocument) =
JsObject(bson.elements.map(elem => elem._1 -> myJson(elem._2)))
implicit val docWrites = OWrites[BSONDocument](jsonDoc)
def myJson(value: BSONValue): JsValue = value match {
case BSONDateTime(value) = ???
case doc @ BSONDocument(_) => jsonDoc(doc)
case bson => BSONFormats.toJSON(bson)
}
}
/* where needed */ import MyImplicits.dateWrites