Overview
In this guide, you can learn about Codecs and the supporting classes that
handle the encoding and decoding of Kotlin objects to and from BSON data
in the MongoDB Kotlin driver. The Codec abstraction allows you to map any Kotlin type to
a corresponding BSON type. You can use this to map your domain objects
directly to and from BSON instead of using data classes or an intermediate
map-based object such as Document or BsonDocument.
You can learn how to specify custom encoding and decoding logic using
the Codec abstraction and view example implementations in the following
sections:
Codec
The Codec interface contains abstract methods for serializing and
deserializing Kotlin objects to BSON data. You can define your conversion logic
between BSON and your Kotlin object in your implementation of this interface.
To implement the Codec interface, override the encode(), decode(),
and getEncoderClass() abstract methods.
The encode() method requires the following parameters:
Parameter Type | Description |
|---|---|
| An instance of a class that implements |
| The data that your implementation encodes. The type must match the type variable assigned to your implementation. |
| Contains meta information about the Kotlin object data that it encodes to BSON including whether to store the current value in a MongoDB collection. |
This method uses the BsonWriter instance to send the encoded value to
MongoDB and does not return a value.
The decode() method returns your Kotlin object instance populated with the
value from the BSON data. This method requires the following parameters:
Parameter Type | Description |
|---|---|
| An instance of a class that implements |
| Contains information about the BSON data that it decodes to a Kotlin object. |
The getEncoderClass() method returns a class instance of the Kotlin class
since Kotlin cannot infer the type due to type erasure.
See the following code examples that show how you can implement a custom
Codec.
The PowerStatus enum contains the values "ON" and "OFF" to represent
the states of an electrical switch.
enum class PowerStatus { ON, OFF }
The PowerStatusCodec class implements Codec in order to convert
the Kotlin enum values to corresponding BSON boolean values. The
encode() method converts a PowerStatus to a BSON boolean and the
decode() method performs the conversion in the opposite direction.
class PowerStatusCodec : Codec<PowerStatus> { override fun encode(writer: BsonWriter, value: PowerStatus, encoderContext: EncoderContext) = writer.writeBoolean(value == PowerStatus.ON) override fun decode(reader: BsonReader, decoderContext: DecoderContext): PowerStatus { return when (reader.readBoolean()) { true -> PowerStatus.ON false -> PowerStatus.OFF } } override fun getEncoderClass(): Class<PowerStatus> = PowerStatus::class.java }
You can add an instance of the PowerStatusCodec to your CodecRegistry
which contains a mapping between your Codec and the Kotlin object type to
which it applies. Continue to the CodecRegistry
section of this page to see how you can include your Codec.
For more information about the classes and interfaces in this section, see the following API Documentation:
CodecRegistry
A CodecRegistry is an immutable collection of Codec instances that
encode and decode the Kotlin classes they specify. You can use any of the
following CodecRegistries class static factory methods to construct a
CodecRegistry from the Codec instances contained in the associated
types:
fromCodecs()fromProviders()fromRegistries()
The following code snippet shows how to construct a CodecRegistry using
the fromCodecs() method:
val codecRegistry = CodecRegistries.fromCodecs(IntegerCodec(), PowerStatusCodec())
In the preceding example, we assign the CodecRegistry the following Codec
implementations:
IntegerCodec, aCodecthat convertsIntegersand is part of the BSON package.PowerStatusCodec, our sample
Codecthat converts Kotlin enum values to BSON booleans.
You can retrieve the Codec instances from the CodecRegistry instance
from the prior example using the following code:
val powerStatusCodec = codecRegistry.get(PowerStatus::class.java) val integerCodec = codecRegistry.get(Integer::class.java)
If you attempt to retrieve a Codec instance for a class that is not
registered, the get() method throws a CodecConfigurationException
exception.
For more information about the classes and interfaces in this section, see the following API Documentation:
CodecProvider
A CodecProvider is an interface that contains abstract methods that create
Codec instances and assign them to a CodecRegistry instance. Similar
to the CodecRegistry, the BSON library uses the Codec instances
retrieved by the get() method to convert between Kotlin and BSON data types.
However, in cases in which you add a class that contains fields that require
corresponding Codec objects, you need to ensure that you instantiate the
Codec objects for the class' fields before you instantiate the
Codec for the class. You can use the CodecRegistry parameter in
the get() method to pass any of the Codec instances that the
Codec relies on.
The following code example shows how you can implement CodecProvider to
pass the MonolightCodec any Codec instances it needs in a
CodecRegistry instance such as the PowerStatusCodec from our prior
example:
class MonolightCodec(registry: CodecRegistry) : Codec<Monolight> { private val powerStatusCodec: Codec<PowerStatus> private val integerCodec: Codec<Int> init { powerStatusCodec = registry[PowerStatus::class.java] integerCodec = IntegerCodec() } override fun encode(writer: BsonWriter, value: Monolight, encoderContext: EncoderContext) { writer.writeStartDocument() writer.writeName("powerStatus") powerStatusCodec.encode(writer, value.powerStatus, encoderContext) writer.writeName("colorTemperature") integerCodec.encode(writer, value.colorTemperature, encoderContext) writer.writeEndDocument() } override fun decode(reader: BsonReader, decoderContext: DecoderContext): Monolight { val monolight = Monolight() reader.readStartDocument() while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) { when (reader.readName()) { "powerStatus" -> monolight.powerStatus = powerStatusCodec.decode(reader, decoderContext) "colorTemperature" -> monolight.colorTemperature = integerCodec.decode(reader, decoderContext) "_id" -> reader.readObjectId() } } reader.readEndDocument() return monolight } override fun getEncoderClass(): Class<Monolight> = Monolight::class.java }
To see a runnable example that demonstrates read and write operations using
these Codec classes, see the Custom Codec Example
section of this guide.
Default Codec Registry
The default codec registry is a set of CodecProvider classes that
specify conversion between commonly-used Kotlin and MongoDB types. The
driver automatically uses the default codec registry unless you specify
a different one.
If you need to override the behavior of one or more Codec classes, but
keep the behavior from the default codec registry for the other classes,
you can specify all of the registries in order of precedence. For example,
suppose you wanted to override the default provider behavior of a Codec for
enum types with your custom MyEnumCodec, you must add it to the registry
list prior to the default codec registry as shown in the example below:
val newRegistry = CodecRegistries.fromRegistries( CodecRegistries.fromCodecs(MyEnumCodec()), MongoClientSettings.getDefaultCodecRegistry() )
For more information about the classes and interfaces in this section, see the following API documentation sections:
BsonTypeClassMap
The BsonTypeClassMap class contains a recommended mapping between BSON
and Kotlin types. You can use this class in your custom Codec or
CodecProvider to help you manage which Kotlin types to decode your BSON
types to container classes that implement Iterable or Map such as
the Document class.
You can add or modify the BsonTypeClassMap default mapping by passing a
Map containing new or replacement entries.
The following code snippet shows how you can retrieve the Kotlin class type
that corresponds to the BSON type in the default BsonTypeClassMap
instance:
val bsonTypeClassMap = BsonTypeClassMap() val clazz = bsonTypeClassMap[BsonType.ARRAY] println("Class name: " + clazz.name)
Java type: java.util.List
You can modify these mappings in your instance by specifying replacements in the
BsonTypeClassMap constructor. The following code snippet shows how
you can replace the mapping for ARRAY in your BsonTypeClassMap
instance with the Set class:
val replacements = mutableMapOf<BsonType, Class<*>>(BsonType.ARRAY to MutableSet::class.java) val bsonTypeClassMap = BsonTypeClassMap(replacements) val clazz = bsonTypeClassMap[BsonType.ARRAY] println("Class name: " + clazz.name)
Java type: java.util.Set
For a complete list of the default mappings, refer to the BsonTypeClassMap API Documentation.
For an example of how the Document class uses BsonTypeClassMap, see
the driver source code for the following classes:
Custom Codec Example
In this section, we show how you can implement Codec and CodecProvider
to define the encoding and decoding logic for a custom Kotlin class. We also show
how you can specify and use your custom implementations to perform insert
and retrieve operations.
Tip
Kotlin Serialization
As an alternative to implementing custom codecs, you can use
Kotlin serialization to handle your data encoding and decoding with
@Serializable classes. You might choose Kotlin serialization if you are
already familiar with the framework or prefer to use an idiomatic Kotlin approach.
See the Kotlin Serialization
documentation for more information.
The following code snippet shows our example custom class called Monolight
and its fields that we want to store and retrieve from a MongoDB collection:
data class Monolight( var powerStatus: PowerStatus = PowerStatus.OFF, var colorTemperature: Int? = null ) { override fun toString(): String = "Monolight [powerStatus=$powerStatus, colorTemperature=$colorTemperature]" }
This class contains the following fields, each of which we need to assign a
Codec:
powerStatusdescribes whether the light is switched "on" or "off" for which we use the PowerStatusCodec that converts specific enum values to BSON booleans.colorTemperaturedescribes the color of the light and contains anIntvalue for which we use theIntegerCodecincluded in the BSON library.
The following code example shows how we can implement a Codec for the
Monolight class. Note that the constructor expects an instance of
CodecRegistry from which it retrieves the Codec instances it needs
to encode and decode its fields:
class MonolightCodec(registry: CodecRegistry) : Codec<Monolight> { private val powerStatusCodec: Codec<PowerStatus> private val integerCodec: Codec<Int> init { powerStatusCodec = registry[PowerStatus::class.java] integerCodec = IntegerCodec() } override fun encode(writer: BsonWriter, value: Monolight, encoderContext: EncoderContext) { writer.writeStartDocument() writer.writeName("powerStatus") powerStatusCodec.encode(writer, value.powerStatus, encoderContext) writer.writeName("colorTemperature") integerCodec.encode(writer, value.colorTemperature, encoderContext) writer.writeEndDocument() } override fun decode(reader: BsonReader, decoderContext: DecoderContext): Monolight { val monolight = Monolight() reader.readStartDocument() while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) { when (reader.readName()) { "powerStatus" -> monolight.powerStatus = powerStatusCodec.decode(reader, decoderContext) "colorTemperature" -> monolight.colorTemperature = integerCodec.decode(reader, decoderContext) "_id" -> reader.readObjectId() } } reader.readEndDocument() return monolight } override fun getEncoderClass(): Class<Monolight> = Monolight::class.java }
To ensure we make the Codec instances for the fields available for
Monolight, we implement a custom CodecProvider shown in the following
code example:
class MonolightCodecProvider : CodecProvider { override fun <T> get(clazz: Class<T>, registry: CodecRegistry): Codec<T>? { return if (clazz == Monolight::class.java) { MonolightCodec(registry) as Codec<T> } else null // Return null when not a provider for the requested class } }
After defining the conversion logic, we can perform the following:
Store data from instances of
Monolightinto MongoDBRetrieve data from MongoDB into instances of
Monolight
The following example class contains code that assigns the
MonolightCodecProvider to the MongoCollection instance by passing it
to the withCodecRegistry() method. The example class also inserts and
retrieves data using the Monolight class and associated codecs:
fun main() = runBlocking { val mongoClient = MongoClient.create("<connection string uri>") val codecRegistry = CodecRegistries.fromRegistries( CodecRegistries.fromCodecs(IntegerCodec(), PowerStatusCodec()), CodecRegistries.fromProviders(MonolightCodecProvider()), MongoClientSettings.getDefaultCodecRegistry() ) val database = mongoClient.getDatabase("codecs_example_products") val collection = database.getCollection<Monolight>("monolights") .withCodecRegistry(codecRegistry) // Construct and insert an instance of Monolight val myMonolight = Monolight(PowerStatus.ON, 5200) collection.insertOne(myMonolight) // Retrieve one or more instances of Monolight val lights = collection.find().toList() println(lights) }
[Monolight [powerStatus=ON, colorTemperature=5200]]
For more information about the methods and classes mentioned in this section, see the following API Documentation: