So far, we have only tried to serialize and deserialize simple types. What if we wanted to decode the language field in the repository array to an enumeration rather than a string? We might, for instance, define the following enumeration:
scala> object Language extends Enumeration { val Scala, Java, JavaScript = Value } defined object Language
Casbah lets us define custom serializers tied to a specific Scala type: we can inform Casbah that whenever it encounters an instance of the Language.Value
type in a DBObject
, the instance should be passed through a custom transformer that will convert it to, for instance, a string, before writing it to the database.
To define a custom serializer, we need to define a class that extends the Transformer
trait. This trait exposes a single method, transform(o:AnyRef):AnyRef
. Let's define a LanguageTransformer
trait that transforms from Language.Value
to String
:
scala> import org.bson.{BSON, Transformer} import org.bson.{BSON, Transformer} scala> trait LanguageTransformer extends Transformer { def transform(o:AnyRef):AnyRef = o match { case l:Language.Value => l.toString case _ => o } } defined trait LanguageTransformer
We now need to register the trait to be used whenever an instance of type Language.Value
needs to be decoded. We can do this using the addEncodingHook
method:
scala> BSON.addEncodingHook( classOf[Language.Value], new LanguageTransformer {})
We can now construct DBObject
instances containing values of the Language
enumeration:
scala> val repoObj = DBObject( "github_id" -> 1234L, "language" -> Language.Scala ) repoObj: DBObject = { "github_id" : 1234 , "language" : "Scala"}
What about the reverse? How do we tell Casbah to read the "language"
field as Language.Value
? This is not possible with custom deserializers: "Scala"
is now stored as a string in the database. Thus, when it comes to deserialization, "Scala"
is no different from, say, "mojombo"
. We thus lose type information when "Scala"
is serialized.
Thus, while custom encoding hooks are useful for serialization, they are much less useful when deserializing. A cleaner, more consistent alternative to customize both serialization and deserialization is to use type classes. We have already covered how to use these extensively in Chapter 5, Scala and SQL through JDBC, in the context of serializing to and from SQL. The procedure here would be very similar:
MongoReader[T]
type class with a read(v:Any)
:T
method.MongoReader
in the MongoReader
companion object for all types of interest, such as String
, Language.Value
.DBObject
with a read[T:MongoReader]
method using the pimp my library pattern.For instance, the implementation of MongoReader
for Language.Value
would be as follows:
implicit object LanguageReader extends MongoReader[Language.Value] { def read(v:Any):Language.Value = v match { case s:String => Language.withName(s) } }
We could then do the same with a MongoWriter
type class. Using type classes is an idiomatic and extensible approach to custom serialization and deserialization.
We provide a complete example of type classes in the code examples associated with this chapter (in the typeclass
directory).