MongoScala3Codec – Compile‑time BSON codecs for Scala 3. Auto-generates type-safe BSON codecs at compile time with zero runtime overhead and production-ready error handling.
This library was created to enable native MongoDB usage in Scala 3. The official mongo-scala-driver only supports Scala 2.11, 2.12, and 2.13 because it relies heavily on Scala 2 macros for automatic codec generation. Since Scala 3 completely redesigned the macro system, the official driver requires a major rewrite to support Scala 3.
Your options without MongoScala3Codec:
- ⬇️ Downgrade to Scala 2.13 (lose Scala 3 features)
- ❌ Wait indefinitely for official Scala 3 support
✅ Zero Boilerplate - One line registers any case class ✅ Compile-Time Safe - Catch errors before deployment, not in production ✅ BSON-Native - Preserves ObjectId, Binary, Decimal128, Dates ✅ Scala 3 Enums - Full support with string/ordinal/custom field encoding ✅ Production-Ready - Comprehensive error messages, 280+ tests, stress-tested
| Feature | MongoScala3Codec | mongo-scala-driver | ReactiveMongo |
|---|---|---|---|
| Scala 3 Support | ✅ Native | ❌ Scala 2 only¹ | ❌ Scala 2 only² |
| Macro System | ✅ Scala 3 macros | ❌ Scala 2 macros³ | |
| Compile-Time Codecs | ✅ Zero overhead | ✅ Scala 2 only | |
| Type-Safe Field Paths | ✅ MongoPath⁵ | ❌ | ❌ |
| None Handling Options | ✅ Ignore/Encode | ✅ Ignore/Encode | ✅ |
| Production Error Messages | ✅ Detailed⁶ |
Footnotes:
- mongo-scala-driver supports Scala 2.11, 2.12, 2.13 only - Scaladex
- ReactiveMongo v0.20.13 supports Scala 2.11, 2.12, 2.13 only - Scaladex
- mongo-scala-driver "heavily uses macros which were dropped in Scala 3" - Stack Overflow
- ReactiveMongo uses both compile-time macros and runtime reflection components
- Compile-time safe field paths:
MongoPath.of[User](_.address.?.city)respects@BsonProperty - Enhanced macro errors with ❌/✅ examples, runtime errors with causes and suggestions
Bottom line: MongoScala3Codec is the only library that enables native MongoDB usage in Scala 3 with compile-time safety and BSON-native types.
- Strong Type Safety: Compile-time validation of all BSON serialization
- High Performance: Optimized code generation with specialized primitive fast paths
- Minimal Boilerplate: No manual codec writing - everything auto-generated
- Type-Safe Field Paths:
MongoPath.of[User](_.address.?.city)- unique in Scala - Flexible Configuration:
ignoreNonevsencodeNone, custom discriminators - Pure Scala 3: Opaque types, extension methods, modern macro system
Here's everything you need - just copy, paste, and run:
import org.bson.types.ObjectId
import io.github.mbannour.mongo.codecs.{RegistryBuilder, CodecConfig, NoneHandling}
import org.mongodb.scala.MongoClient
case class Address(street: String, city: String, zipCode: Int)
case class Person(_id: ObjectId, name: String, address: Address, email: Option[String])
val registry = RegistryBuilder
.from(MongoClient.DEFAULT_CODEC_REGISTRY)
.ignoreNone
.registerAll[(Address, Person)]
.build
val mongoClient = MongoClient("mongodb://localhost:27017")
val database = mongoClient.getDatabase("myapp").withCodecRegistry(registry)
val people = database.getCollection[Person]("people")
val person = Person(new ObjectId(), "Alice", Address("123 Main", "NYC", 10001), Some("[email protected]"))
people.insertOne(person).toFuture()
val found = people.find().first().toFuture()That's it! No manual codec writing, no reflection, no runtime overhead.
👉 See Quickstart for more examples and explanations.
👉 Complete Documentation Index - Navigation guide for all docs
| Getting Started | Advanced | Reference |
|---|---|---|
| Quickstart | Enum Support | BSON Type Mapping |
| Feature Overview | How It Works | MongoDB Interop |
| FAQ & Troubleshooting | Migration Guide |
💡 New to the library? Start with QUICKSTART.md
- ✅ Automatic BSON codec generation for Scala 3 case classes
- ✅ Support for default parameter values - missing fields use defaults automatically
- ✅ Support for options and nested case classes
- ✅ Custom field name annotations (e.g.,
@BsonProperty) - ✅ Compile-time safe MongoDB field path extraction via
MongoPath - ✅ Scala 3 enum support via
EnumValueCodec - ✅ UUID and Float primitive types built-in support
- ✅ Complete primitive type coverage (Byte, Short, Char)
- ✅ Type-safe configuration with
CodecConfig - ✅ Flexible None handling (encode as null or omit from document)
- ✅ Collections support (List, Set, Vector, Map)
- ✅ Testing utilities with
CodecTestKit
Case class codecs are generated at compile time for speed and safety - no runtime reflection overhead.
Fluent, immutable builder for CodecRegistry:
- Register single or multiple types
- Add explicit codecs
- Merge builders
- Choose how
Option[None]is handled:
| Setting | BSON Result |
|---|---|
NoneHandling.Encode |
Encodes None as null |
NoneHandling.Ignore |
Omits the field entirely |
Compile-time safe field paths (respects @BsonProperty) - prevents stringly-typed bugs.
Provided via EnumValueCodecProvider (by name or ordinal).
Testing helpers for round-trip checks and structure assertions.
val reg = MongoClient.DEFAULT_CODEC_REGISTRY
.newBuilder
.register[MyType]
.buildval reg = MongoClient.DEFAULT_CODEC_REGISTRY
.newBuilder
.registerAll[(Address, Person, Task)]
.buildTip: Prefer registerAll[(A, B, C)] over many sequential register calls for better performance.
val reg = MongoClient.DEFAULT_CODEC_REGISTRY
.newBuilder
.ignoreNone // or .encodeNone
.registerAll[(Address, Person)]
.buildval isProd = sys.env.get("APP_ENV").contains("prod")
val reg = MongoClient.DEFAULT_CODEC_REGISTRY
.newBuilder
.register[CommonType]
.registerIf[ProdOnlyType](isProd)
.buildval common = MongoClient.DEFAULT_CODEC_REGISTRY.newBuilder
.register[Address]
.register[Person]
val extra = MongoClient.DEFAULT_CODEC_REGISTRY.newBuilder
.register[Department]
val reg = (common ++ extra).buildScala 3 enums are supported via EnumValueCodecProvider.
import io.github.mbannour.mongo.codecs.EnumValueCodecProvider
import org.bson.codecs.configuration.CodecRegistries.{fromProviders, fromRegistries}
import org.mongodb.scala.MongoClient
enum Priority:
case Low, Medium, High
val base = fromRegistries(
MongoClient.DEFAULT_CODEC_REGISTRY,
fromProviders(EnumValueCodecProvider.forStringEnum[Priority])
)
val reg = RegistryBuilder
.from(base)
.register[Task]
.buildforStringEnum[E]→ stores enum by its name (stable, readable)forOrdinalEnum[E]→ stores enum by its ordinal (compact, renumbering-sensitive)
Best practice: Prefer string-based enums (forStringEnum) for schema stability and readability.
Avoid stringly-typed bugs in filters, updates, projections, and sorts.
import io.github.mbannour.fields.MongoPath
import io.github.mbannour.fields.MongoPath.syntax.? // Option hop
import org.mongodb.scala.model.Filters
import org.mongodb.scala.bson.annotations.BsonProperty
import org.bson.types.ObjectId
case class Address(street: String, @BsonProperty("zip") zipCode: Int)
case class User(_id: ObjectId, name: String, address: Option[Address])
val zipPath = MongoPath.of[User](_.address.?.zipCode) // "address.zip"
val filter = Filters.equal(zipPath, 12345)
val idPath = MongoPath.of[User](_._id) // "_id"
val namePath = MongoPath.of[User](_.name) // "name"- Use simple access chains, e.g.
_.a.b.c - Import
MongoPath.syntax.?to transparently traverseOption @BsonPropertyvalues on constructor params are respected
Opaque types work out of the box (zero runtime overhead).
object Domain:
opaque type UserId = String
object UserId:
def apply(v: String): UserId = v
extension (u: UserId) def value: String = u
import Domain.*
import org.bson.types.ObjectId
case class Profile(_id: ObjectId, userId: UserId, age: Int)
val reg = MongoClient.DEFAULT_CODEC_REGISTRY
.newBuilder
.register[Profile]
.buildimport io.github.mbannour.mongo.codecs.{CodecTestKit, RegistryBuilder, CodecConfig, NoneHandling}
import org.bson.codecs.Codec
import org.bson.types.ObjectId
import org.mongodb.scala.MongoClient
case class User(_id: ObjectId, name: String, email: Option[String])
given CodecConfig = CodecConfig(noneHandling = NoneHandling.Ignore)
val reg = RegistryBuilder
.from(MongoClient.DEFAULT_CODEC_REGISTRY)
.register[User]
.build
given Codec[User] = reg.get(classOf[User])
// Round-trip symmetry
CodecTestKit.assertCodecSymmetry(User(new ObjectId(), "Alice", Some("[email protected]")))
// Inspect BSON
val bson = CodecTestKit.toBsonDocument(User(new ObjectId(), "Bob", None))
println(bson.toJson()) // email omitted due to Ignore✅ Catch codec bugs early (no DB needed)
✅ Validate BSON structure deterministically
✅ Works with ScalaTest, MUnit, ScalaCheck
enum types with EnumValueCodecProvider.
status: PaymentStatus) are not supported yet. Use concrete types in fields, or wrappers that you register explicitly.
List[PaymentStatus]) are not supported yet. Use collections of concrete members.
BsonValue.
Add to your build.sbt:
libraryDependencies += "io.github.mbannour" %% "mongoscala3codec" % "0.0.7"For use with MongoDB Scala Driver:
libraryDependencies ++= Seq(
"io.github.mbannour" %% "mongoscala3codec" % "0.0.7",
("org.mongodb.scala" %% "mongo-scala-driver" % "5.6.0").cross(CrossVersion.for3Use2_13)
)Requirements:
- Scala 3.3.1 or higher
- JDK 11 or higher
See the Quickstart for a hands-on tutorial, or jump straight to the Feature Overview for comprehensive examples.
MongoScala3Codec includes JMH microbenchmarks for measuring codec performance. The benchmarks cover:
- Flat case classes with primitives
- Nested structures with
Optionfields - Case class hierarchies with manual discriminators
- Large collections (List, Vector, Map)
See Benchmarks Documentation for details on running benchmarks and interpreting results.
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
This project is licensed under the MIT License - see the LICENSE file for details.