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.
- Quick Start
- Why MongoScala3Codec?
- Installation
- Features
- Documentation
- Usage Examples
- Architecture
- Performance
- Contributing
- Support & Community
- License
1. Add dependency to build.sbt:
libraryDependencies ++= Seq(
"io.github.mbannour" %% "mongoscala3codec" % "0.0.11",
("org.mongodb.scala" %% "mongo-scala-driver" % "5.6.0").cross(CrossVersion.for3Use2_13)
)2. 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("alice@example.com"))
people.insertOne(person).toFuture()That's it! No manual codec writing, no reflection, no runtime overhead.
👉 New to the library? Continue with Quickstart Guide for detailed walkthrough.
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
- ✍️ Write manual codecs for every type (100+ lines per case class)
| 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.
Add to your build.sbt:
libraryDependencies += "io.github.mbannour" %% "mongoscala3codec" % "0.0.11"For use with MongoDB Scala Driver:
libraryDependencies ++= Seq(
"io.github.mbannour" %% "mongoscala3codec" % "0.0.11",
("org.mongodb.scala" %% "mongo-scala-driver" % "5.6.0").cross(CrossVersion.for3Use2_13)
)Requirements:
- Scala 3.3.1 or higher
- JDK 11 or higher
- ✅ Zero Boilerplate - One line registers any case class
- ✅ Compile-Time Safe - Catch errors before deployment, not in production
- ✅ Zero Runtime Overhead - Codecs generated at compile time, no reflection
- ✅ Type-Safe Field Paths -
MongoPath.of[User](_.address.?.city)- unique in Scala - ✅ BSON-Native - Preserves ObjectId, Binary, Decimal128, Dates
- ✅ Production-Ready - Comprehensive error messages, 280+ tests, stress-tested
- ✅ Sealed Traits/Classes - Polymorphic codecs with automatic discriminators (v0.0.8)
- ✅ Scala 3 Enums - Full support with string/ordinal/custom field encoding
- ✅ Default Parameters - Missing fields use defaults automatically
- ✅ Options & Nested Types -
Option[T], nested case classes, collections - ✅ Opaque Types - Zero-cost wrappers work seamlessly
- ✅ Primitive Types - Complete coverage (Byte, Short, Char, Int, Long, Float, Double, Boolean, String)
- ✅ UUID & Binary Types - Built-in support for common data types
- ✅ Collections - List, Set, Vector, Map with proper BSON encoding
- ✅ Flexible None Handling - Encode as null or omit from document
- ✅ Custom Field Names -
@BsonPropertyannotations - ✅ Batch Registration -
registerAll[(A, B, C)]for optimal compile times - ✅ Conditional Registration -
registerIf[T](condition)for environment-specific codecs - ✅ Testing Utilities -
CodecTestKitfor round-trip validation - ✅ Type-Safe Configuration - Immutable
CodecConfigwith builder pattern
👉 Complete Documentation Index - Navigation hub for all documentation
| I want to... | Documentation |
|---|---|
| Get started quickly | Quickstart Guide |
| Understand all features | Feature Overview |
| Work with sealed traits | Sealed Trait Support |
| Use Scala 3 enums | Enum Support |
| Understand BSON mapping | BSON Type Mapping |
| Fix compilation errors | FAQ & Troubleshooting |
| Integrate with MongoDB | MongoDB Interop |
| Migrate from another library | Migration Guide |
| Understand internals | How It Works |
val reg = RegistryBuilder
.from(MongoClient.DEFAULT_CODEC_REGISTRY)
.register[MyType]
.buildval reg = RegistryBuilder
.from(MongoClient.DEFAULT_CODEC_REGISTRY)
.registerAll[(Address, Person, Task)]
.buildTip: Prefer registerAll[(A, B, C)] over sequential register calls for better compile-time performance.
val reg = RegistryBuilder
.from(MongoClient.DEFAULT_CODEC_REGISTRY)
.ignoreNone // Omit None fields, or use .encodeNone to encode as null
.registerAll[(Address, Person)]
.build| Setting | BSON Result |
|---|---|
NoneHandling.Encode (.encodeNone) |
Encodes None as null |
NoneHandling.Ignore (.ignoreNone) |
Omits the field entirely |
val isProd = sys.env.get("APP_ENV").contains("prod")
val reg = RegistryBuilder
.from(MongoClient.DEFAULT_CODEC_REGISTRY)
.register[CommonType]
.registerIf[ProdOnlyType](isProd)
.buildval common = RegistryBuilder
.from(MongoClient.DEFAULT_CODEC_REGISTRY)
.register[Address]
.register[Person]
val extra = RegistryBuilder
.from(MongoClient.DEFAULT_CODEC_REGISTRY)
.register[Department]
val reg = (common ++ extra).build✅ Sealed traits and classes are fully supported! Use registerSealed[T] for automatic polymorphic codec generation:
sealed trait Animal
case class Dog(name: String, breed: String) extends Animal
case class Cat(name: String, lives: Int) extends Animal
val registry = RegistryBuilder
.from(MongoClient.DEFAULT_CODEC_REGISTRY)
.registerSealed[Animal] // Registers Animal + all subtypes
.build
// Works polymorphically with automatic discriminator field
val animals: List[Animal] = List(
Dog("Rex", "Labrador"),
Cat("Whiskers", 9)
)
database.getCollection[Animal]("animals").insertMany(animals).toFuture()Batch Registration:
// Register multiple sealed traits efficiently
val registry = RegistryBuilder
.from(MongoClient.DEFAULT_CODEC_REGISTRY)
.registerSealedAll[(Animal, Vehicle, Status)]
.buildFeatures:
- ✅ Automatic discriminator field (default:
_type, configurable) - ✅ Single call registers entire hierarchy
- ✅ Works with collections, nested structures, and Option fields
- ✅ Supports sealed trait, sealed class, and sealed abstract class
Limitations:
⚠️ Case objects in sealed hierarchies are not supported - use case classes or Scala 3 enums⚠️ Sealed traits with type parameters are not yet supported
👉 See Sealed Trait Support Guide for comprehensive examples.
Scala 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]
.buildEnum Variants:
forStringEnum[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.
👉 See Enum Support Guide for advanced patterns.
Avoid stringly-typed bugs in filters, updates, projections, and sorts with MongoPath.
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"Rules:
- 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 = RegistryBuilder
.from(MongoClient.DEFAULT_CODEC_REGISTRY)
.register[Profile]
.buildTest your codecs without a database using CodecTestKit:
import 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("a@x.com")))
// Inspect BSON
val bson = CodecTestKit.toBsonDocument(User(new ObjectId(), "Bob", None))
println(bson.toJson()) // email omitted due to IgnoreWhy Use It?
✅ Catch codec bugs early (no DB needed) ✅ Validate BSON structure deterministically ✅ Works with ScalaTest, MUnit, ScalaCheck
MongoScala3Codec leverages Scala 3's inline macros and metaprogramming for compile-time codec generation:
┌─────────────────────┐
│ Scala 3 Case Class │
│ case class User │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Inline Macros │
│ (Compile-Time) │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ BSON Codec │
│ (Zero Overhead) │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ MongoDB Driver │
│ BsonDocument │
└─────────────────────┘
Key Design Principles:
- Compile-Time Generation - All codec logic generated at compile time, no runtime reflection
- Type Safety - Invalid configurations caught by compiler, not at runtime
- Zero Overhead - Generated code is as efficient as hand-written codecs
- Incremental Compilation - Smart caching reduces recompilation time
- Clear Error Messages - Actionable compile-time and runtime diagnostics
👉 See How It Works for detailed architecture explanation.
MongoScala3Codec includes JMH microbenchmarks for measuring codec performance. The benchmarks cover:
- Flat case classes with primitives
- Nested structures with
Optionfields - Sealed trait hierarchies with discriminators
- Large collections (List, Vector, Map)
Headline Results:
- Encode Performance: ~500,000 ops/sec for simple case classes
- Decode Performance: ~400,000 ops/sec for simple case classes
- Memory: Zero allocation overhead vs hand-written codecs
- Compile Time: Batch registration reduces compile time by 60%
👉 See Benchmarks Documentation for details on running benchmarks and interpreting results.
Contributions are welcome! We appreciate:
- 🐛 Bug reports and fixes
- 📚 Documentation improvements
- ✨ Feature implementations
- 🧪 Test coverage enhancements
- 💡 Ideas and feedback
Before contributing:
- Check existing issues for duplicates
- Read CONTRIBUTING.md for guidelines
- Follow the Code of Conduct
Development Setup:
git clone https://github.com/mbannour/MongoScala3Codec.git
cd MongoScala3Codec
sbt compile
sbt test- GitHub Discussions - Q&A, ideas, and general discussion
- Issue Tracker - Bug reports and feature requests
- Changelog - Release notes and migration guides
- GitHub Releases - Version announcements
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
Built with ❤️ for the Scala 3 community. Special thanks to all contributors who have helped improve this library.
Star ⭐ this repo if you find it useful!
Made with Scala 3 | Powered by Inline Macros | Zero Runtime Overhead
Documentation • Quickstart • GitHub • Issues