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.
- Sealed Trait Support: Automatic codec generation for sealed trait hierarchies.
- Case Class Support: Automatic codec generation for case classes, including concrete implementations from sealed trait hierarchies.
- Flexible Configuration: Type-safe configuration for codec behavior.
- Pure Scala 3: Built with opaque types and extension methods for idiomatic Scala 3 code.
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 5-Minute Quickstart for more examples and explanations.
- 5-Minute Quickstart - Get started immediately
- BSON Type Mapping - Complete type reference (35+ types)
- Feature Overview - Complete feature guide with examples
- MongoDB Interop - Driver integration guide
- How It Works - Scala 3 derivation internals explained
- Migration Guide - Migrate from other libraries
- FAQ & Troubleshooting - Common issues and solutions
- β Automatic BSON codec generation for Scala 3 case classes
- β Support for concrete case classes from sealed trait hierarchies - each case class registered independently
- β Support for default parameter values - missing fields use defaults automatically
- β Support for options and nested case classes
- β Sealed trait hierarchies with concrete case class implementations
- β Polymorphic sealed trait/class fields NOT supported - see limitations below
- β
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
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-M2"For use with MongoDB Scala Driver:
libraryDependencies ++= Seq(
"io.github.mbannour" %% "mongoscala3codec" % "0.0.7-M2",
("org.mongodb.scala" %% "mongo-scala-driver" % "5.2.1").cross(CrossVersion.for3Use2_13)
)Requirements:
- Scala 3.3.1 or higher
- JDK 11 or higher
See the 5-Minute 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 - Sealed trait hierarchies (ADTs)
- 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.
- Performance: Optimized
RegistryBuilderwith efficient caching (O(N) total vs O(NΒ²)) - New: Convenience methods
just[T],withTypes[T],registerIf[T] - New: State inspection methods (
currentConfig,codecCount,hasCodecFor, etc.) - New: JMH benchmarks for performance testing
- Enhancement:
MongoPathfor compile-time safe field paths - Breaking: Simplified
CodecConfigby removing discriminator field - Improvement: Better builder composition with
++operator
- Initial stable release
- Case class codec generation
- Sealed trait support
- Optional field handling
- Custom field names with
@BsonProperty - Scala 3 enum support
- Opaque type support
- Testing utilities with
CodecTestKit
- π Documentation
- π Report Issues
- π¬ Discussions