mcodec

GenCodec-style serialization for Scala 3 — a format-agnostic, streaming Input/Output core with type class derivation built on M&DE. Ships with a JSON backend; BSON and CBOR Input/ Output implementations exist in-tree but aren't wired up to a public read/write entry point yet.

Experimental. Pinned to Scala 3.9.0, required by Made (made_3:0.5.1). Cross-built for JVM, Scala.js, and Scala Native.

Overview

mcodec is a serialization library in the spirit of AVSystem GenCodec: a single MCodec[T] type class both reads and writes, and the wire format is decoupled from the codecs. Codecs talk to an abstract streaming Input/Output, so the same MCodec[T] works across any backend that implements those.

  • One type class for read and writeMCodec[T] with read(input) / write(output, value)
  • Format-agnostic core — codecs target a streaming Input/Output; the JSON backend is just one implementation
  • Automatic derivationderives MCodec for case classes, enums, and sealed hierarchies, powered by M&DE mirrors
  • Annotation-aware@name to rename fields and ADT cases on the wire, @flatten / @defaultCase for ADTs, @stringEnum to encode an enum case by name, @outOfOrder to accept a field regardless of wire position, and @transientDefault to omit a field from output when it equals its declared default
  • Combinatorstransform, transformed, nullable, and makeLazy for building codecs from existing ones
  • Explicit nulls — compiled with -Yexplicit-nulls; nullability is expressed in the types

Built-in codecs cover the primitives, BigInt/BigDecimal, UUID, Option, Either, tuples, and the common collections (List, Vector, Seq, Set, Map).

Installation

Published to Maven Central under com.halotukozak.

scala-cli

//> using scala 3.9.0
//> using dep com.halotukozak::mcodec::0.2.0

sbt

scalaVersion := "3.9.0"
libraryDependencies += "com.halotukozak" %% "mcodec" % "0.2.0"

mill

def scalaVersion = "3.9.0"
def mvnDeps = Seq(mvn"com.halotukozak::mcodec::0.2.0")

Quickstart

Derive an MCodec for a case class and round-trip it through JSON.

import halotukozak.mcodec.*

case class User(name: String, age: Int) derives MCodec

val json = Json.write(User("Alice", 30)) // {"name":"Alice","age":30}
val user = Json.read[User](json) // User("Alice", 30)

Rename fields and ADT cases on the wire with @name:

import halotukozak.mcodec.*
import halotukozak.made.annotation.name

case class User(@name("user_name") name: String, age: Int) derives MCodec

enum Shape derives MCodec:
  @name("circ") case Circle(radius: Double)

Json.write(User("bob", 30)) // {"user_name":"bob","age":30}
Json.write[Shape](Shape.Circle(2.0)) // {"circ":{"radius":2.0}}

Build codecs from existing ones with the combinators on MCodec:

import halotukozak.mcodec.*

opaque type Email = String
given MCodec[Email] = MCodec[String].transform(identity, identity)

val nullableInt: MCodec[Int | Null] = MCodec[Int].nullable

Build

scala-cli --power compile . --exclude benchmark
scala-cli --power test . --exclude benchmark
scala-cli --power fmt .

--exclude benchmark keeps the separate benchmark build (competitor deps, JMH) out of the library build — scala-cli has no directive for this.

Benchmarks

The benchmarks guide compares mcodec's compile time and serialization throughput against circe, jsoniter-scala, uPickle, zio-json, borer, play-json and AVSystem GenCodec (the design mcodec is modelled on). The suite lives in benchmark/ (a separate scala-cli build) and is regenerated with benchmark/scripts/run_all.sh.

Documentation

Guides and API reference: mcodec.halotukozak.com.

Acknowledgements

mcodec is inspired by the AVSystem commons by **ghik **, whose GenCodec is the model for the codec design