Reactive Scala Driver for MongoDB

Asynchronous & Non-Blocking

BSON readers and writers

In order to get and store data with MongoDB, ReactiveMongo provides an extensible API to define appropriate readers and writers.

As long as you are working with BSONValues, some default implementations of readers and writers are provided by the following import.

import reactivemongo.api.bson._

Custom reader

Getting values follows the same principle using getAs(String) method. This method is parametrized with a type that can be transformed into a BSONValue using a BSONReader instance that is implicitly available in the scope (again, the default readers are already imported if you imported reactivemongo.api.bson._.) If the value could not be found, or if the reader could not deserialize it (often because the type did not match), None will be returned.

import reactivemongo.api.bson.BSONString

val albumTitle2 = album2.getAsTry[String]("title")
// Some("Everybody Knows this is Nowhere")

val albumTitle3 = album2.getAsTry[BSONString]("title")
// Some(BSONString("Everybody Knows this is Nowhere"))

In order to read values of custom types. To do so, a custom instance of BSONReader, or of BSONDocumentReader, must be resolved (in the implicit scope).

A BSONReader for a custom value class:

package object custom {
  class Score(val value: Float) extends AnyVal

  import reactivemongo.api.bson._

  implicit object ScoreReader extends BSONReader[Score] {
    def readTry(bson: BSONValue) =
      bson.asTry[BSONNumberLike].flatMap(_.toFloat).map(new Score(_))
  }
}

When reading a numeric value from MongoDB, it’s recommended to use the typeclass BSONNumberLike, to benefit from numeric conversions it provides.

Once a custom BSONReader (or BSONDocumentReader) is defined, it can be used in aDocument.getAsTry[MyValueType]("docProperty").

A BSONDocumentReader for a custom case class:

import reactivemongo.api.bson._

implicit object PersonReader extends BSONDocumentReader[Person] {
  def readDocument(bson: BSONDocument) = for {
    name <- bson.getAsTry[String]("fullName")
    age <- bson.getAsTry[BSONNumberLike]("personAge").flatMap(_.toInt)
  } yield new Person(name, age)
}

Once a custom BSONDocumentReader can be resolved, it can be used when working with a query result.

import scala.concurrent.{ ExecutionContext, Future }
import reactivemongo.api.bson.{ BSONDocument, BSONDocumentReader }
import reactivemongo.api.bson.collection.BSONCollection

// Provided the `Person` case class is defined, with its `BSONDocumentReader`
implicit def personReader: BSONDocumentReader[Person] = ???

def findPerson(personCollection: BSONCollection, name: String)(implicit ec: ExecutionContext): Future[Option[Person]] = personCollection.find(BSONDocument("fullName" -> name)).one[Person]

See how to find documents.

Custom writer

Of course it’s also possible to write a value of a custom type, a custom instance of BSONWriter, or of BSONDocumentWriter must be available.

import reactivemongo.api.bson._

case class Score(value: Float)

implicit object ScoreWriter extends BSONWriter[Score] {
  def writeTry(score: Score) =
    scala.util.Success(BSONDouble(score.value))
}

// Uses `BSONDouble` to write `Float`,
// for compatibility with MongoDB numeric values

Each value that can be written using a BSONWriter can be used directly when calling a BSONDocument constructor.

val album2 = reactivemongo.api.bson.BSONDocument(
  "title" -> "Everybody Knows this is Nowhere",
  "releaseYear" -> 1969)

Note that this does not use implicit conversions, but rather implicit type classes.

import reactivemongo.api.bson._

// Declare it as implicit for resolution
val personWriter0: BSONDocumentWriter[Person] =
  BSONDocumentWriter[Person] { person =>
    BSONDocument("fullName" -> person.name, "personAge" -> person.age)
  }

Once a BSONDocumentWriter is available, an instance of the custom class can be inserted or updated to the MongoDB.

import scala.concurrent.{ ExecutionContext, Future }

import reactivemongo.api.bson.BSONDocumentWriter
import reactivemongo.api.bson.collection.BSONCollection

// Provided the `Person` case class is defined, with its `BSONDocumentWriter`
implicit def personWriter: BSONDocumentWriter[Person] = ???

def create(personCollection: BSONCollection, person: Person)(implicit ec: ExecutionContext): Future[Unit] = {
  val writeResult = personCollection.insert.one(person)
  writeResult.map(_ => {/*once this is successful, just return successfully*/})
}

See how to write documents.

Helpful macros

To ease the definition of reader and writer instances for your custom types, ReactiveMongo provides some helper Macros.

libraryDependencies ++= Seq(
  "org.reactivemongo" %% "reactivemongo-bson-macros" % "0.20.13")
case class Person(name: String, age: Int)

import reactivemongo.api.bson._

implicit val personHandler: BSONHandler[Person] =
  Macros.handler[Person]

/* Or only one of:
implicit val personReader: BSONDocumentReader[Person] = Macros.reader[Person]
implicit val personWriter: BSONDocumentWriter[Person] = Macros.writer[Person]
*/

The BSONHandler provided by Macros.handler gathers both BSONReader and BSONWriter traits.

The Macros.reader can be used to generate only the BSONReader, while the Macros.writer is for BSONWriter.

The A type parameter (e.g. with A being Person, Macros.reader[Person]) defines a type for a case class, or for a sealed trait with subclasses. This type will be the basis for the auto-generated implementation.

Some other types with matching apply-unapply might work but behaviour is undefined. Since macros will match the apply-unapply pair you are free to overload these methods in the companion object.

Case class mapping:

For the case classes, the fields get mapped into BSON properties with respective names, and BSON handlers are pulled from implicit scope to (de)serialize them (in the previous Person example, the handlers for String are resolved for the name property).

So in order to use custom types as properties in case classes, the appropriate handlers are in scope.

For example if you have case class Foo(bar: Bar) and want to create a handler for it is enough to put an implicit handler for Bar in it’s companion object. That handler might itself be macro generated, or written by hand.

The macros are currently limited to case classes whose constructor doesn’t take more than 22 parameters (due to Scala not generating apply and unapply in the other cases).

Sealed trait and union types:

Sealed traits are also supported as union types, with each of their subclasses considered as a disjoint case.

import reactivemongo.api.bson.{ BSONHandler, Macros }

sealed trait Tree
case class Node(left: Tree, right: Tree) extends Tree
case class Leaf(data: String) extends Tree

object Tree {
  implicit val node = Macros.handler[Node]
  implicit val leaf = Macros.handler[Leaf]

  implicit val bson: BSONHandler[Tree] = Macros.handler[Tree]
}

The handler, reader and writer macros each have a corresponding extended macro: readerOpts, writerOpts and handlerOpts.

These ‘Opts’ suffixed macros can be used to explicitly define the UnionType.

sealed trait Color

case object Red extends Color
case object Blue extends Color
case class Green(brightness: Int) extends Color
case class CustomColor(code: String) extends Color

object Color {
  import reactivemongo.api.bson.Macros
  import reactivemongo.api.bson.MacroOptions.{
    AutomaticMaterialization, UnionType, \/
  }

  // Use `UnionType` to define a subset of the `Color` type,
  type PredefinedColor =
    UnionType[Red.type \/ Green \/ Blue.type] with AutomaticMaterialization

  val predefinedColor = Macros.handlerOpts[Color, PredefinedColor]
}

As for the UnionType definition, Foo \/ Bar \/ Baz is interpreted as type Foo or type Bar or type Baz. The option AutomaticMaterialization is used there to automatically try to materialize the handlers for the sub-types (disabled by default).

The other options available to configure the typeclasses generation at compile time are the following.

Annotations:

Some annotations are also available to configure the macros.

The @Key annotation allows to specify the field name for a class property.

For example, it is convenient to use when you’d like to leverage the MongoDB _id index but you don’t want to actually use _id in your code.

import reactivemongo.api.bson.Macros.Annotations.Key

case class Website(@Key("_id") url: String)
// Generated handler will map the `url` field in your code to as `_id` field

The @Ignore can be applied on the class properties to be ignored.

import reactivemongo.api.bson.Macros.Annotations.Ignore

case class Foo(
  bar: String,
  @Ignore lastAccessed: Long = -1L
)

The @Flatten can be used to indicate to the macros that the representation of a property must be flatten rather than a nested document.

import reactivemongo.api.bson.BSONDocument
import reactivemongo.api.bson.Macros.Annotations.Flatten

case class Range(start: Int, end: Int)

case class LabelledRange(
  name: String,
  @Flatten range: Range)

// Flattened with macro as bellow:
BSONDocument("name" -> "foo", "start" -> 0, "end" -> 1)

// Rather than:
// BSONDocument("name" -> "foo", "range" -> BSONDocument(
//   "start" -> 0, "end" -> 1))

Troubleshooting:

The mapped type can also be defined inside other classes, objects or traits but not inside functions (known macro limitation). In order to work you should have the case class in scope (where you call the macro), so you can refer to it by it’s short name - without package. This is necessary because the generated implementations refer to it by the short name to support nested declarations. You can work around this with local imports.

object lorem {
  case class Ipsum(v: String)
}

implicit val handler = {
  import lorem.Ipsum
  reactivemongo.api.bson.Macros.handler[Ipsum]
}

Provided handlers

The following handlers are provided by ReactiveMongo, to read and write the BSON values.

BSON type Value type
BSONArray Any collection
BSONBinary Array[Byte]
BSONBoolean Boolean
BSONDocument Map[K, V]
BSONDateTime java.util.Date
BSONDouble Double
BSONInteger Int
BSONLong Long
BSONString String

Optional value

An optional value can be added to a document using the Option type (e.g. for an optional string, Option[String]).

Using BSONBooleanLike, it is possible to read the following BSON values as boolean.

BSON type Rule
BSONInteger true if > 0
BSONDouble true if > 0
BSONNull always false
BSONUndefined always false

Using BSONNumberLike, it is possible to read the following BSON values as number.

Concrete examples

Troubleshooting

When using the compiler option -Ywarn-unused and the BSON macro (e.g. Macros.handler), you can get a warning as bellow. It can be safely ignore (there for compatibility).

private val in <$anon: reactivemongo.api.bson.BSONDocumentReader[foo.Bar] with reactivemongo.api.bson.BSONDocumentWriter[foo.Bar] with reactivemongo.api.bson.BSONHandler[reactivemongo.api.bson.BSONDocument,foo.Bar]> is never used

If the following compilation error is raised, then reactivemongo-bson-macros must be add to the build.

[error] /path/to.scala:L:C: object Macros is not a member of package reactivemongo.api.bson
[error]   import reactivemongo.api.bson.Macros

Previous: Overview of the ReactiveMongo BSON library

Suggest changes