Vulcan

Vulcan

  • API Docs
  • Documentation
  • GitHub

›Documentation

Documentation

  • Overview
  • Codecs
  • Modules

Modules

The following sections describe the additional modules.

Enumeratum

The vulcan-enumeratum module provides Codecs for Enumeratum enumerations.

For regular Enums, also mix in VulcanEnum to derive a Codec instance.

import enumeratum.{Enum, EnumEntry, VulcanEnum}
import enumeratum.EnumEntry.Lowercase
import vulcan.Codec
import vulcan.generic.{AvroDoc, AvroNamespace}

@AvroNamespace("com.example")
@AvroDoc("The different card suits")
sealed trait Suit extends EnumEntry with Lowercase

object Suit extends Enum[Suit] with VulcanEnum[Suit] {
  case object Clubs extends Suit
  case object Diamonds extends Suit
  case object Hearts extends Suit
  case object Spades extends Suit

  val values = findValues
}

Codec[Suit]
// res1: Codec[Suit] = WithTypeName(
//   codec = Validated(
//     codec = Codec({
//   "type" : "enum",
//   "name" : "Suit",
//   "namespace" : "com.example",
//   "doc" : "The different card suits",
//   "symbols" : [ "clubs", "diamonds", "hearts", "spades" ]
// }),
//     validSchema = {"type":"enum","name":"Suit","namespace":"com.example","doc":"The different card suits","symbols":["clubs","diamonds","hearts","spades"]}
//   ),
//   typeName = "com.example.Suit"
// )

Annotations like @AvroDoc can be used to customize the derivation.

For ValueEnums, also mix in the matching VulcanValueEnum to derive a Codec instance.

import enumeratum.values.{StringEnum, StringEnumEntry, StringVulcanEnum}

@AvroNamespace("com.example")
@AvroDoc("The available colors")
sealed abstract class Color(val value: String) extends StringEnumEntry

object Color extends StringEnum[Color] with StringVulcanEnum[Color] {
  case object Red extends Color("red")
  case object Green extends Color("green")
  case object Blue extends Color("blue")

  val values = findValues
}

Codec[Color]
// res2: Codec.Aux[vulcan.Avro.EnumSymbol, Color] = WithTypeName(
//   codec = Validated(
//     codec = Codec({
//   "type" : "enum",
//   "name" : "Color",
//   "namespace" : "com.example",
//   "doc" : "The available colors",
//   "symbols" : [ "red", "green", "blue" ]
// }),
//     validSchema = {"type":"enum","name":"Color","namespace":"com.example","doc":"The available colors","symbols":["red","green","blue"]}
//   ),
//   typeName = "com.example.Color"
// )

For StringVulcanEnum, the enumeration is encoded as an Avro enumeration. For the other VulcanValueEnums (ByteVulcanEnum, CharVulcanEnum, IntVulcanEnum, LongVulcanEnum, and ShortVulcanEnum), the encoding relies on the encoding of the enumeration's value type (Byte, Char, Int, Long, and Short).

import enumeratum.values.{IntEnum, IntEnumEntry, IntVulcanEnum}

sealed abstract class Day(val value: Int) extends IntEnumEntry

object Day extends IntEnum[Day] with IntVulcanEnum[Day] {
  case object Monday extends Day(1)
  case object Tuesday extends Day(2)
  case object Wednesday extends Day(3)
  case object Thursday extends Day(4)
  case object Friday extends Day(5)
  case object Saturday extends Day(6)
  case object Sunday extends Day(7)

  val values = findValues
}

Codec[Day]
// res3: Codec.Aux[Int, Day] = ImapErrors(
//   codec = Codec("int"),
//   f = enumeratum.values.Vulcan$$$Lambda$17251/0x00000001048c1840@23399f63,
//   g = vulcan.Codec$$Lambda$16858/0x000000010469c840@3eceb9fe
// )

Generic

The vulcan-generic module provides generic derivation of Codecs using Magnolia for records and unions, and reflection for enumerations and fixed types.

To derive Codecs for case classes or sealed traits, we can use Codec.derive. Annotations like @AvroDoc and @AvroNamespace can be used to customize the documentation and namespace during derivation.

import vulcan.generic._

@AvroNamespace("com.example")
@AvroDoc("Person with a first name, last name, and optional age")
final case class Person(firstName: String, lastName: String, age: Option[Int])

Codec.derive[Person]
// res4: Codec[Person] = WithTypeName(
//   codec = Validated(
//     codec = Codec({
//   "type" : "record",
//   "name" : "Person",
//   "namespace" : "com.example",
//   "doc" : "Person with a first name, last name, and optional age",
//   "fields" : [ {
//     "name" : "firstName",
//     "type" : "string"
//   }, {
//     "name" : "lastName",
//     "type" : "string"
//   }, {
//     "name" : "age",
//     "type" : [ "null", "int" ]
//   } ]
// }),
//     validSchema = {"type":"record","name":"Person","namespace":"com.example","doc":"Person with a first name, last name, and optional age","fields":[{"name":"firstName","type":"string"},{"name":"lastName","type":"string"},{"name":"age","type":["null","int"]}]}
//   ),
//   typeName = "com.example.Person"
// )

While case classes correspond to Avro records, sealed traits correspond to unions.

sealed trait FirstOrSecond

@AvroNamespace("com.example")
final case class First(value: Int) extends FirstOrSecond

@AvroNamespace("com.example")
final case class Second(value: String) extends FirstOrSecond

Codec.derive[FirstOrSecond]
// res5: Codec.Aux[Any, FirstOrSecond] = WithTypeName(
//   codec = Validated(
//     codec = WithTypeName(
//       codec = Validated(
//         codec = UnionCodec(
//           alts = Append(
//             leftNE = Singleton(a = vulcan.Codec$Alt$$anon$6@2be5228),
//             rightNE = Singleton(a = vulcan.Codec$Alt$$anon$6@7d74dd76)
//           )
//         ),
//         validSchema = [{"type":"record","name":"First","namespace":"com.example","fields":[{"name":"value","type":"int"}]},{"type":"record","name":"Second","namespace":"com.example","fields":[{"name":"value","type":"string"}]}]
//       ),
//       typeName = "union"
//     ),
//     validSchema = [{"type":"record","name":"First","namespace":"com.example","fields":[{"name":"value","type":"int"}]},{"type":"record","name":"Second","namespace":"com.example","fields":[{"name":"value","type":"string"}]}]
//   ),
//   typeName = "repl.MdocSession.MdocApp0.FirstOrSecond"
// )

Shapeless Coproducts are also supported and correspond to Avro unions.

import shapeless.{:+:, CNil}

Codec[Int :+: String :+: CNil]
// res6: Codec[Int :+: String :+: CNil] = WithTypeName(
//   codec = Validated(
//     codec = UnionCodec(
//       alts = Append(
//         leftNE = Singleton(a = vulcan.Codec$Alt$$anon$6@2d07fd36),
//         rightNE = Singleton(a = vulcan.Codec$Alt$$anon$6@422fec96)
//       )
//     ),
//     validSchema = ["int","string"]
//   ),
//   typeName = "Coproduct"
// )

deriveEnum can be used to partly derive Codecs for enumeration types. Annotations like @AvroDoc can be used to customize the derivation.

import vulcan.AvroError
import vulcan.generic.{AvroDoc, AvroNamespace}

@AvroNamespace("com.example")
@AvroDoc("A selection of different fruits")
sealed trait Fruit
case object Apple extends Fruit
case object Banana extends Fruit
case object Cherry extends Fruit

deriveEnum[Fruit](
  symbols = List("apple", "banana", "cherry"),
  encode = {
    case Apple  => "apple"
    case Banana => "banana"
    case Cherry => "cherry"
  },
  decode = {
    case "apple"  => Right(Apple)
    case "banana" => Right(Banana)
    case "cherry" => Right(Cherry)
    case other    => Left(AvroError(s"$other is not a Fruit"))
  }
)
// res7: Codec.Aux[vulcan.Avro.EnumSymbol, Fruit] = WithTypeName(
//   codec = Validated(
//     codec = Codec({
//   "type" : "enum",
//   "name" : "Fruit",
//   "namespace" : "com.example",
//   "doc" : "A selection of different fruits",
//   "symbols" : [ "apple", "banana", "cherry" ]
// }),
//     validSchema = {"type":"enum","name":"Fruit","namespace":"com.example","doc":"A selection of different fruits","symbols":["apple","banana","cherry"]}
//   ),
//   typeName = "com.example.Fruit"
// )

deriveFixed can be used to partly derive Codecs for fixed types. Annotations like @AvroDoc can be used to customize the derivation.

import vulcan.AvroError

sealed abstract case class Pence(value: Byte)

object Pence {
  def apply(value: Byte): Either[AvroError, Pence] =
    if(0 <= value && value < 100) Right(new Pence(value) {})
    else Left(AvroError(s"Expected pence value, got $value"))
}

deriveFixed[Pence](
  size = 1,
  encode = pence => Array[Byte](pence.value),
  decode = bytes => Pence(bytes.head)
)
// res8: Codec.Aux[vulcan.Avro.Fixed, Pence] = WithTypeName(
//   codec = Validated(
//     codec = Codec({
//   "type" : "fixed",
//   "name" : "Pence",
//   "namespace" : "repl.MdocSession.MdocApp0",
//   "size" : 1
// }),
//     validSchema = {"type":"fixed","name":"Pence","namespace":"repl.MdocSession.MdocApp0","size":1}
//   ),
//   typeName = "repl.MdocSession.MdocApp0.Pence"
// )

Refined

The vulcan-refined module provides Codecs for refined refinement types.

Refinement types are encoded using their base type (e.g. Int for PosInt). When decoding, Codecs check to ensure values conform to the predicate of the refinement type (e.g. Positive for PosInt), and raise an error for values which do not conform.

import eu.timepit.refined.auto._
import eu.timepit.refined.types.numeric.PosInt
import vulcan.refined._

Codec[PosInt]
// res9: Codec[eu.timepit.refined.api.Refined[Int, eu.timepit.refined.numeric.Positive]] = ImapErrors(
//   codec = Codec("int"),
//   f = vulcan.refined.package$$$Lambda$17274/0x00000001048d7040@10c8bc02,
//   g = vulcan.Codec$$Lambda$16858/0x000000010469c840@6a6126da
// )

Codec.encode[PosInt](1)
// res10: Either[AvroError, Codec[eu.timepit.refined.api.Refined[Int, eu.timepit.refined.numeric.Positive]]#AvroType] = Right(
//   value = 1
// )

Codec.decode[PosInt](0)
// res11: Either[AvroError, PosInt] = Left(
//   value = AvroError(Predicate failed: (0 > 0).)
// )
← Codecs
  • Enumeratum
  • Generic
  • Refined

Copyright © 2019-2025 OVO Energy Limited.
Icon designed by Kiranshastry from Flaticon.