MongoScala3Codec is a macro-based library for BSON serialization and deserialization of Scala 3 case classes. It generates BSON codecs at compile time, ensuring:
- Strong Type Safety: Compile-time validation of BSON serialization.
- High Performance: Optimized code generation for efficient BSON handling.
- Minimal Boilerplate: No need to write manual codec definitions.
Note:
- Only Scala 3 case classes are supported. Sealed traits (ADTs) are NOT supported.
- For Scala 3 enums, use
EnumValueCodecProvider
to register a codec for your enum type.- Not all Scala 3 enum types are supported. See the summary table below for details on which enum types are supported and which require workarounds.
- Only plain enums (no parameters, no ADT/sealed traits, no custom fields) are fully supported. See the table below for a summary of supported and unsupported enum types.
- Automatic BSON codec generation for Scala 3 case classes
- Support for default values, options, and nested case classes
- Custom field name annotations (e.g.,
@BsonProperty
) - Compile-time safe MongoDB field path extraction via
MongoFieldResolver
- Scala 3 enum support via
EnumValueCodecProvider
Add to your build.sbt
:
libraryDependencies += "io.github.mbannour" %% "mongoscala3codec" % "0.0.5"
import org.bson.types.ObjectId
import org.mongodb.scala.bson.annotations.BsonProperty
case class Address(street: String, city: String, zipCode: Int)
case class Person(
_id: ObjectId,
@BsonProperty("n") name: String,
age: Int,
address: Option[Address]
)
enum Priority:
case Low, Medium, High
case class Task(_id: ObjectId, title: String, priority: Priority)
import org.bson.codecs.configuration.{CodecRegistries, CodecRegistry}
import io.github.mbannour.mongo.codecs.CodecProviderMacro
import io.github.mbannour.mongo.codecs.EnumValueCodecProvider
import org.mongodb.scala.MongoClient
val personProvider = CodecProviderMacro.createCodecProviderEncodeNone[Person]
val addressProvider = CodecProviderMacro.createCodecProviderEncodeNone[Address]
val taskProvider = CodecProviderMacro.createCodecProviderEncodeNone[Task]
val priorityEnumProvider = EnumValueCodecProvider[Priority, String](
toValue = _.toString,
fromValue = str => Priority.valueOf(str)
)
val codecRegistry: CodecRegistry = CodecRegistries.fromRegistries(
CodecRegistries.fromProviders(personProvider, addressProvider, taskProvider, priorityEnumProvider),
MongoClient.DEFAULT_CODEC_REGISTRY
)
given CodecRegistry = codecRegistry // 👈 DO NOT FORGET THIS LINE!
val mongoClient = MongoClient()
val database = mongoClient.getDatabase("test_db").withCodecRegistry(codecRegistry)
val peopleCollection = database.getCollection[Person]("people")
val taskCollection = database.getCollection[Task]("tasks")
val person = Person(new ObjectId(), "Alice", 30, Some(Address("Main St", "City", 12345)))
peopleCollection.insertOne(person)
val task = Task(new ObjectId(), "Complete report", Priority.High)
taskCollection.insertOne(task)
val foundPerson = peopleCollection.find().first().head()
val foundTask = taskCollection.find().first().head()
MongoFieldResolver
enables compile-time safe extraction of MongoDB field names, including nested structures and custom field renaming.
import io.github.mbannour.fields.MongoFieldMapper
val dbField = MongoFieldMapper.asMap[Person]("address.city")
If you pass a field that doesn't exist, an exception is thrown with a helpful message.
- See the GitHub Wiki for guides, tutorials, and references.
- For a full working example, see the integration tests in
integration/src/test/scala/io/github/mbannour/mongo/codecs/CodecProviderIntegrationSpec.scala
. - Important Limitations:
- Only Scala 3 case classes are supported. Sealed traits (ADTs) are NOT supported.
- For Scala 3 enums, use
EnumValueCodecProvider
to register a codec for your enum type. - Not all Scala 3 enum types are supported. See the summary table below for details on which enum types are supported and which require workarounds.
- Only plain enums (no parameters, no ADT/sealed traits, no custom fields) are fully supported. See the table below for a summary of supported and unsupported enum types.
import io.github.mbannour.mongo.codecs.EnumValueCodecProvider
import org.bson.codecs.StringCodec
given Codec[String] = new StringCodec() // 1) supply a String codec
val enumProvider = EnumValueCodecProvider.forStringEnum[Priority] // 2) done!
For custom representations:
import org.bson.codecs.IntegerCodec
given Codec[Int] = new IntegerCodec()
val provider = EnumValueCodecProvider.forOrdinalEnum[Priority]
And if you need total control:
EnumValueCodecProvider[Priority, Boolean](
_.ordinal == 0, // toValue
bool => if bool then Priority.Low else Priority.High // fromValue
)
You are now covering all practical cases for serializing Scala 3 enums as BSON with MongoDB, as long as the enums:
- Are “plain” enums (not parameterized, not ADTs/sealed traits, not enums with additional fields).
Enum Type | Supported? | Helper to Use |
---|---|---|
Plain enum (no params) | Yes | forStringEnum, forOrdinalEnum |
Enum with methods/companion | Yes | as above |
Enum with parameters (ADT style) | No | Use your own codec |
Enum with custom value per case | No | Use your own codec |
MongoDB does not natively support Scala 3 sealed traits or ADT-style enums. If you want to represent ADTs or sealed traits in MongoDB, you should:
- Refactor your ADT/sealed trait to a plain Scala 3 enum if possible.
- Use a custom codec (see below) if you need to serialize/deserialize ADT-style enums or sealed traits.
- For plain enums, use the built-in helpers:
EnumValueCodecProvider.forStringEnum[YourEnum]
(stores as string)EnumValueCodecProvider.forOrdinalEnum[YourEnum]
(stores as ordinal)
Example: Transforming a sealed trait to an enum
// Original ADT
sealed trait Priority
case object Low extends Priority
case object Medium extends Priority
case object High extends Priority
// Scala 3 enum equivalent
enum Priority:
case Low, Medium, High
Register the enum codec:
import io.github.mbannour.mongo.codecs.EnumValueCodecProvider
import org.bson.codecs.StringCodec
given Codec[String] = new StringCodec() // 👈 required for string-based enum codecs
val enumProvider = EnumValueCodecProvider.forStringEnum[Priority]
Note:
- If your ADT has parameters or custom fields, you must write your own codec using
CodecProviderMacro
or a manual implementation. - See the summary table above for supported enum types.
Follow these steps to make sure your codecs work for all your case classes, enums, and nested types!
import org.bson.types.ObjectId
final case class Address(street: String, city: String, zipCode: Int)
final case class Person(_id: ObjectId, name: String, age: Int, address: Option[Address])
enum Priority:
case Low, Medium, High
final case class Task(_id: ObjectId, title: String, priority: Priority)
import org.mongodb.scala.MongoClient
import org.bson.codecs.StringCodec
import io.github.mbannour.mongo.codecs.{CodecProviderMacro, EnumValueCodecProvider}
import org.bson.codecs.configuration.{CodecRegistries, CodecRegistry}
List every type you want to store, including all case classes and enums (and nested types!).
object Codecs:
given org.bson.codecs.Codec[String] = new StringCodec()
private val allProviders = Seq(
CodecProviderMacro.createCodecProviderEncodeNone[Address],
CodecProviderMacro.createCodecProviderEncodeNone[Person],
CodecProviderMacro.createCodecProviderEncodeNone[Task],
EnumValueCodecProvider.forStringEnum[Priority]
)
val registry: CodecRegistry = CodecRegistries.fromRegistries(
CodecRegistries.fromProviders(allProviders*),
MongoClient.DEFAULT_CODEC_REGISTRY
)
This step is ESSENTIAL: It makes your custom registry discoverable by macros and the MongoDB driver at runtime. If you skip it, nested (de)serialization will fail!
given CodecRegistry = registry // DO NOT FORGET THIS LINE!
val client = MongoClient().withCodecRegistry(Codecs.registry)
val db = client.getDatabase("mydb").withCodecRegistry(Codecs.registry)
val people = db.getCollection[Person]("people")
val person = Person(new ObjectId(), "Alice", 30, Some(Address("Main St", "City", 12345)))
people.insertOne(person).toFuture() // No more "No codec found" errors!
Q: I get No codec found for type: my.model.Address
or a similar error.
A: Check that you:
- Listed every type as a provider (including nested case classes/enums)
- Included those providers in your registry
- Added
given CodecRegistry = registry
at the end of your Codecs object - Passed your custom registry everywhere (client, db, collection)
- Add codec providers for every type (including nested)
- Build the combined registry
- Expose as
given CodecRegistry = registry
- Use your registry everywhere
By following these steps, your codecs will “just work” for any Scala 3 case class, enum, or nested structure!
Contributions are welcome! Please fork the repository and submit a pull request. For major changes, open an issue first to discuss your ideas.
This project is licensed under the MIT License. See the LICENSE file for details.
Main Developer: Mohamed Ali Bannour
Email: [email protected]