mbannour / mongoscala3codec   0.0.8

Apache License 2.0 GitHub

Compile-time generated BSON codecs for Scala 3 case classes, ensuring seamless and type-safe integration with MongoDB

Scala versions: 3.x

MongoScala3Codec

mongoScala3Codec version mongoScala3Codec compatibility Build Status License

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.


πŸ“‹ Table of Contents


πŸš€ Quick Start

1. Add dependency to build.sbt:

libraryDependencies ++= Seq(
  "io.github.mbannour" %% "mongoscala3codec" % "0.0.8",
  ("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("[email protected]"))
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.


πŸ’‘ Why MongoScala3Codec?

The Core Problem: No Scala 3 Support

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)

MongoScala3Codec Solves This

Feature MongoScala3Codec mongo-scala-driver ReactiveMongo
Scala 3 Support βœ… Native ❌ Scala 2 onlyΒΉ ❌ Scala 2 onlyΒ²
Macro System βœ… Scala 3 macros ❌ Scala 2 macrosΒ³ ⚠️ Scala 2 macros
Compile-Time Codecs βœ… Zero overhead βœ… Scala 2 only ⚠️ Mixed⁴
Type-Safe Field Paths βœ… MongoPath⁡ ❌ ❌
None Handling Options βœ… Ignore/Encode βœ… Ignore/Encode βœ…
Production Error Messages βœ… Detailed⁢ ⚠️ Basic ⚠️ Basic

Footnotes:

  1. mongo-scala-driver supports Scala 2.11, 2.12, 2.13 only - Scaladex
  2. ReactiveMongo v0.20.13 supports Scala 2.11, 2.12, 2.13 only - Scaladex
  3. mongo-scala-driver "heavily uses macros which were dropped in Scala 3" - Stack Overflow
  4. ReactiveMongo uses both compile-time macros and runtime reflection components
  5. Compile-time safe field paths: MongoPath.of[User](_.address.?.city) respects @BsonProperty
  6. 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.


πŸ“¦ Installation

Add to your build.sbt:

libraryDependencies += "io.github.mbannour" %% "mongoscala3codec" % "0.0.8"

For use with MongoDB Scala Driver:

libraryDependencies ++= Seq(
  "io.github.mbannour" %% "mongoscala3codec" % "0.0.8",
  ("org.mongodb.scala" %% "mongo-scala-driver" % "5.6.0").cross(CrossVersion.for3Use2_13)
)

Requirements:

  • Scala 3.3.1 or higher
  • JDK 11 or higher

✨ Features

Core Capabilities

  • βœ… 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

Type Support

  • βœ… 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

Configuration & Tools

  • βœ… Flexible None Handling - Encode as null or omit from document
  • βœ… Custom Field Names - @BsonProperty annotations
  • βœ… Batch Registration - registerAll[(A, B, C)] for optimal compile times
  • βœ… Conditional Registration - registerIf[T](condition) for environment-specific codecs
  • βœ… Testing Utilities - CodecTestKit for round-trip validation
  • βœ… Type-Safe Configuration - Immutable CodecConfig with builder pattern

πŸ“š Documentation

πŸ“– Complete Guide

πŸ‘‰ Complete Documentation Index - Navigation hub for all documentation

🎯 Quick Links by Use Case

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

πŸ’» Usage Examples

Basic Registration

Single Types

val reg = RegistryBuilder
  .from(MongoClient.DEFAULT_CODEC_REGISTRY)
  .register[MyType]
  .build

Batch Types (Recommended)

val reg = RegistryBuilder
  .from(MongoClient.DEFAULT_CODEC_REGISTRY)
  .registerAll[(Address, Person, Task)]
  .build

Tip: Prefer registerAll[(A, B, C)] over sequential register calls for better compile-time performance.

Configure Option Handling

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

Conditional Registration

val isProd = sys.env.get("APP_ENV").contains("prod")

val reg = RegistryBuilder
  .from(MongoClient.DEFAULT_CODEC_REGISTRY)
  .register[CommonType]
  .registerIf[ProdOnlyType](isProd)
  .build

Merging Builders

val 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

βœ… 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)]
  .build

Features:

  • βœ… 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.


Enums

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]
  .build

Enum 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.


Type-Safe Field Paths

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 traverse Option
  • @BsonProperty values on constructor params are respected

Opaque Types

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]
  .build

Testing

Test 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("[email protected]")))

// Inspect BSON
val bson = CodecTestKit.toBsonDocument(User(new ObjectId(), "Bob", None))
println(bson.toJson())  // email omitted due to Ignore

Why Use It?

βœ… Catch codec bugs early (no DB needed) βœ… Validate BSON structure deterministically βœ… Works with ScalaTest, MUnit, ScalaCheck


πŸ—οΈ Architecture

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:

  1. Compile-Time Generation - All codec logic generated at compile time, no runtime reflection
  2. Type Safety - Invalid configurations caught by compiler, not at runtime
  3. Zero Overhead - Generated code is as efficient as hand-written codecs
  4. Incremental Compilation - Smart caching reduces recompilation time
  5. Clear Error Messages - Actionable compile-time and runtime diagnostics

πŸ‘‰ See How It Works for detailed architecture explanation.


πŸ† Performance & Benchmarks

MongoScala3Codec includes JMH microbenchmarks for measuring codec performance. The benchmarks cover:

  • Flat case classes with primitives
  • Nested structures with Option fields
  • 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.


🀝 Contributing

Contributions are welcome! We appreciate:

  • πŸ› Bug reports and fixes
  • πŸ“š Documentation improvements
  • ✨ Feature implementations
  • πŸ§ͺ Test coverage enhancements
  • πŸ’‘ Ideas and feedback

Before contributing:

  1. Check existing issues for duplicates
  2. Read CONTRIBUTING.md for guidelines
  3. Follow the Code of Conduct

Development Setup:

git clone https://github.com/mbannour/MongoScala3Codec.git
cd MongoScala3Codec
sbt compile
sbt test

πŸ›Ÿ Support & Community

πŸ“– Documentation

πŸ’¬ Get Help

πŸ“° Stay Updated


πŸ“œ License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.


πŸ™ Acknowledgments

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