ScalaCheck Derivation
Derives Arbitrary, Cogen, and Shrink instances for case classes, sealed traits, Scala 3 enums, Java enums, and more. Replaces manual instance definitions and scalacheck-shapeless.
Installation
sbt
Cross-platform (JVM / Scala.js / Scala Native):
Note
You also need scalacheck as a runtime dependency:
Quick start
Generating random case class instances
//> using scala 2.13.18
//> using dep com.kubuszok::kindlings-scalacheck-derivation:0.2.0
//> using dep org.scalacheck::scalacheck:1.19.0
import hearth.kindlings.scalacheckderivation._
import hearth.kindlings.scalacheckderivation.extensions._
import org.scalacheck.Arbitrary
case class Person(name: String, age: Int)
// Extension method on Arbitrary companion
implicit val arbPerson: Arbitrary[Person] = Arbitrary.derived[Person]
// Generate a random sample — derivation produces valid instances
val sample = Arbitrary.arbitrary[Person].sample
println(sample.isDefined)
// expected output:
// true
API
Derivation methods
ScalaCheck derivation works via extension methods on the ScalaCheck companion objects and via standalone Derive* objects:
| Method | Returns | Description |
|---|---|---|
Arbitrary.derived[A] |
Arbitrary[A] |
Derive via extension method on companion |
DeriveArbitrary.derived[A] |
Arbitrary[A] |
Derive via standalone object |
Cogen.derived[A] |
Cogen[A] |
Derive via extension method on companion |
DeriveCogen.derived[A] |
Cogen[A] |
Derive via standalone object |
Shrink.derived[A] |
Shrink[A] |
Derive via extension method on companion |
DeriveShrink.derived[A] |
Shrink[A] |
Derive via standalone object |
No configuration class is needed -- derivation is fully automatic based on the type structure.
Supported types
- Case classes -- generates random values for each field
- Sealed traits / enums -- randomly selects a subtype
- Scala 3 enums -- randomly selects an enum value
- Java enums -- randomly selects an enum constant
- Recursive types -- handled automatically, no
Lazywrappers needed - Named tuples -- supported on Scala 3
- Opaque types -- supported when the underlying type has an instance
- Collections and Option -- standard library instances are used
Usage examples
Sealed trait with property-based testing
//> using scala 2.13.18
//> using dep com.kubuszok::kindlings-scalacheck-derivation:0.2.0
//> using dep org.scalacheck::scalacheck:1.19.0
import hearth.kindlings.scalacheckderivation._
import hearth.kindlings.scalacheckderivation.extensions._
import org.scalacheck.{Arbitrary, Prop}
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
implicit val arbShape: Arbitrary[Shape] = Arbitrary.derived[Shape]
val prop = Prop.forAll { (shape: Shape) =>
shape match {
case Circle(_) => true
case Rectangle(_, _) => true
}
}
prop.check()
// + OK, passed 100 tests.
Cogen for function generation
//> using scala 2.13.18
//> using dep com.kubuszok::kindlings-scalacheck-derivation:0.2.0
//> using dep org.scalacheck::scalacheck:1.19.0
import hearth.kindlings.scalacheckderivation._
import hearth.kindlings.scalacheckderivation.extensions._
import org.scalacheck.{Arbitrary, Cogen, Gen}
case class Point(x: Int, y: Int)
// Cogen lets ScalaCheck generate functions *from* your type
implicit val cogenPoint: Cogen[Point] = Cogen.derived[Point]
// Now ScalaCheck can generate Point => String functions
val genFn: Gen[Point => String] = Arbitrary.arbitrary[Point => String]
val fn = genFn.sample.get
println(fn(Point(1, 2)))
Shrink for minimal failing cases
//> using scala 2.13.18
//> using dep com.kubuszok::kindlings-scalacheck-derivation:0.2.0
//> using dep org.scalacheck::scalacheck:1.19.0
import hearth.kindlings.scalacheckderivation._
import hearth.kindlings.scalacheckderivation.extensions._
import org.scalacheck.Shrink
case class Config(retries: Int, timeout: Long, label: String)
implicit val shrinkConfig: Shrink[Config] = Shrink.derived[Config]
// Shrink produces smaller variants of a failing input
val original = Config(100, 5000L, "production")
val shrunk = Shrink.shrink(original).take(5).toList
shrunk.foreach(println)
Recursive data types
//> using scala 2.13.18
//> using dep com.kubuszok::kindlings-scalacheck-derivation:0.2.0
//> using dep org.scalacheck::scalacheck:1.19.0
import hearth.kindlings.scalacheckderivation._
import hearth.kindlings.scalacheckderivation.extensions._
import org.scalacheck.Arbitrary
case class Tree(value: Int, children: List[Tree])
// No Lazy wrapper needed -- recursion is handled automatically
implicit val arbTree: Arbitrary[Tree] = Arbitrary.derived[Tree]
// Recursive types may produce deep trees; retry if sample is None
val sample = Iterator.continually(Arbitrary.arbitrary[Tree].sample).flatten.next()
println(sample)
Debugging
Enable debug logging to see the derivation process:
Or via scalac option:
Comparison with scalacheck-shapeless
Feature differences
| Feature | scalacheck-shapeless | Kindlings |
|---|---|---|
| Same API on Scala 2.13 and 3 | No (Scala 2 only) | Yes |
| Arbitrary derivation | Yes | Yes |
| Cogen derivation | Yes | Yes |
| Shrink derivation | No | Yes |
| Recursive types | Needs Lazy wrappers |
Just works |
| Scala 3 enums | No | Yes |
| Java enums | No | Yes |
| Named tuples | No | Yes |
| Opaque types | No | Yes |
| No shapeless dependency | No | Yes |