API Reference

← Codec Overview | API | Type Rules | Examples | RLP Comparison


Overview

This document provides detailed API reference for the three core traits: ByteEncoder, ByteDecoder, and ByteCodec. For type-specific encoding rules, see Type Rules.

ByteEncoder

ByteEncoder[A] is a contravariant type class for encoding values of type A into deterministic byte sequences.

Core Methods

encode

def encode(value: A): ByteVector

Encodes a value to a deterministic byte sequence.

Example:

import org.sigilaris.core.codec.byte.*

val encoder = ByteEncoder[Long]
encoder.encode(42L)
// res0: ByteVector = Chunk(
//   bytes = View(
//     at = scodec.bits.ByteVector$AtByteBuffer@330ca617,
//     offset = 0L,
//     size = 8L
//   )
// )

Combinators

contramap

def contramap[B](f: B => A): ByteEncoder[B]

Creates a new encoder by applying a function before encoding. This is the contravariant functor operation.

Example:

case class UserId(value: Long)

given ByteEncoder[UserId] = ByteEncoder[Long].contramap(_.value)
ByteEncoder[UserId].encode(UserId(100L))
// res1: ByteVector = Chunk(
//   bytes = View(
//     at = scodec.bits.ByteVector$AtByteBuffer@6ea8b594,
//     offset = 0L,
//     size = 8L
//   )
// )

Use Case: Transform custom types to encodable types.

ByteDecoder

ByteDecoder[A] is a covariant type class for decoding byte sequences into values of type A.

Core Methods

decode

def decode(bytes: ByteVector): Either[DecodeFailure, DecodeResult[A]]

Decodes bytes to a value, returning either a failure or a result with remainder.

DecodeResult:

case class DecodeResult[A](value: A, remainder: ByteVector)

Example:

import scodec.bits.ByteVector

val decoder = ByteDecoder[Long]
val bytes = ByteVector(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a)
decoder.decode(bytes)
// res2: Either[DecodeFailure, DecodeResult[Long]] = Right(
//   value = DecodeResult(
//     value = 42L,
//     remainder = Chunk(
//       bytes = View(
//         at = scodec.bits.ByteVector$AtEmpty$@1e36eb43,
//         offset = 0L,
//         size = 0L
//       )
//     )
//   )
// )

Combinators

map

def map[B](f: A => B): ByteDecoder[B]

Transforms the decoded value using a function. This is the covariant functor operation.

Example:

import org.sigilaris.core.codec.byte.*

case class UserId(value: Long)

given ByteDecoder[UserId] = ByteDecoder[Long].map(UserId(_))
import scodec.bits.ByteVector

val bytes = ByteVector(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64)
ByteDecoder[UserId].decode(bytes)
// res4: Either[DecodeFailure, DecodeResult[UserId]] = Right(
//   value = DecodeResult(
//     value = UserId(value = 100L),
//     remainder = Chunk(
//       bytes = View(
//         at = scodec.bits.ByteVector$AtEmpty$@1e36eb43,
//         offset = 0L,
//         size = 0L
//       )
//     )
//   )
// )

emap

def emap[B](f: A => Either[DecodeFailure, B]): ByteDecoder[B]

Transforms the decoded value with validation. Allows decoding to fail based on business rules.

Example:

import org.sigilaris.core.codec.byte.*
import org.sigilaris.core.failure.DecodeFailure
import cats.syntax.either.*

case class PositiveInt(value: Int)

given ByteDecoder[PositiveInt] = ByteDecoder[Long].emap: n =>
  if n > 0 && n <= Int.MaxValue then
    PositiveInt(n.toInt).asRight
  else
    DecodeFailure(s"Value $n is not a positive Int").asLeft
import scodec.bits.ByteVector

val validBytes = ByteVector(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a)
val invalidBytes = ByteVector(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff)
ByteDecoder[PositiveInt].decode(validBytes)
// res6: Either[DecodeFailure, DecodeResult[PositiveInt]] = Right(
//   value = DecodeResult(
//     value = PositiveInt(value = 10),
//     remainder = Chunk(
//       bytes = View(
//         at = scodec.bits.ByteVector$AtEmpty$@1e36eb43,
//         offset = 0L,
//         size = 0L
//       )
//     )
//   )
// )
ByteDecoder[PositiveInt].decode(invalidBytes).isLeft
// res7: Boolean = true

flatMap

def flatMap[B](f: A => ByteDecoder[B]): ByteDecoder[B]

Chains decoding operations. The next decoder depends on the previously decoded value.

Example:

import org.sigilaris.core.codec.byte.*
import scodec.bits.ByteVector

// Decode length-prefixed data where length determines decoder behavior
def decodeLengthPrefixed: ByteDecoder[String] =
  ByteDecoder[Long].flatMap: length =>
    new ByteDecoder[String]:
      def decode(bytes: ByteVector) =
        if bytes.size >= length then
          val (data, remainder) = bytes.splitAt(length)
          Right(DecodeResult(data.decodeUtf8.getOrElse(""), remainder))
        else
          Left(org.sigilaris.core.failure.DecodeFailure(s"Insufficient bytes: need $length, got ${bytes.size}"))

Use Case: Context-dependent decoding where the structure depends on previously decoded values.

ByteCodec

ByteCodec[A] combines both ByteEncoder[A] and ByteDecoder[A] into a single type class.

trait ByteCodec[A] extends ByteDecoder[A] with ByteEncoder[A]

Usage

When a type has both encoder and decoder instances, you can summon them together:

val codec = ByteCodec[Long]

val encoded = codec.encode(42L)
val decoded = codec.decode(encoded)
encoded
// res9: ByteVector = Chunk(
//   bytes = View(
//     at = scodec.bits.ByteVector$AtByteBuffer@1a4ac35b,
//     offset = 0L,
//     size = 8L
//   )
// )
decoded
// res10: Either[DecodeFailure, DecodeResult[Long]] = Right(
//   value = DecodeResult(
//     value = 42L,
//     remainder = Chunk(
//       bytes = View(
//         at = scodec.bits.ByteVector$AtEmpty$@1e36eb43,
//         offset = 0L,
//         size = 0L
//       )
//     )
//   )
// )

Automatic Derivation

ByteCodec provides automatic derivation for product types (case classes, tuples):

import org.sigilaris.core.codec.byte.*

case class Transaction(from: Long, to: Long, amount: Long)

// Instances automatically derived
val tx = Transaction(1L, 2L, 100L)
val encoded = ByteEncoder[Transaction].encode(tx)
// encoded: ByteVector = ByteVector(24 bytes, 0x000000000000000100000000000000020000000000000064)
val decoded = ByteDecoder[Transaction].decode(encoded)
// decoded: Either[DecodeFailure, DecodeResult[Transaction]] = Right(
//   value = DecodeResult(
//     value = Transaction(from = 1L, to = 2L, amount = 100L),
//     remainder = ByteVector(empty)
//   )
// )

Given Instances

The companion objects provide given instances for common types. See Type Rules for detailed encoding specifications.

Primitive Types

Numeric Types

Collections

Product Types

Extracting Values from DecodeResult

DecodeResult[A] contains both the decoded value and the remainder bytes. To extract just the value:

Example:

import org.sigilaris.core.codec.byte.*
import scodec.bits.ByteVector
val result = ByteDecoder[Long].decode(ByteVector(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a))
// result: Either[DecodeFailure, DecodeResult[Long]] = Right(
//   value = DecodeResult(
//     value = 42L,
//     remainder = Chunk(
//       bytes = View(
//         at = scodec.bits.ByteVector$AtEmpty$@1e36eb43,
//         offset = 0L,
//         size = 0L
//       )
//     )
//   )
// )
result.map(_.value)  // Extract just the value
// res13: Either[DecodeFailure, Long] = Right(value = 42L)

Error Handling

DecodeFailure

Decoding failures are represented by DecodeFailure:

case class DecodeFailure(message: String)

Common failure scenarios:

Example:

import org.sigilaris.core.codec.byte.*
import scodec.bits.ByteVector
// Insufficient bytes for Long (needs 8 bytes)
ByteDecoder[Long].decode(ByteVector(0x01, 0x02))
// res15: Either[DecodeFailure, DecodeResult[Long]] = Left(
//   value = DecodeFailure(
//     msg = "Too short bytes to decode Long; required 8 bytes, but received 2 bytes: ByteVector(2 bytes, 0x0102)"
//   )
// )

// Empty bytes
ByteDecoder[Long].decode(ByteVector.empty)
// res16: Either[DecodeFailure, DecodeResult[Long]] = Left(
//   value = DecodeFailure(
//     msg = "Too short bytes to decode Long; required 8 bytes, but received 0 bytes: ByteVector(empty)"
//   )
// )

Best Practices

1. Use contramap for Encoders

Transform your custom types to standard types:

case class Timestamp(millis: Long)
given ByteEncoder[Timestamp] = ByteEncoder[Long].contramap(_.millis)

2. Use emap for Validation

Add validation logic during decoding:

import org.sigilaris.core.codec.byte.*
import org.sigilaris.core.failure.DecodeFailure
import cats.syntax.either.*

case class PositiveLong(value: Long)

given ByteDecoder[PositiveLong] = ByteDecoder[Long].emap: n =>
  if n > 0 then PositiveLong(n).asRight
  else DecodeFailure(s"Value must be positive, got $n").asLeft

3. Leverage Automatic Derivation

Let the compiler derive instances for case classes:

case class Account(id: Long, balance: BigInt)
// ByteEncoder[Account] and ByteDecoder[Account] automatically available

4. Chain Decoders with flatMap

For complex decoding logic:

ByteDecoder[Long].flatMap: discriminator =>
  discriminator match
    case 1 => ByteDecoder[TypeA]
    case 2 => ByteDecoder[TypeB]
    case _ => ByteDecoder.fail(s"Unknown type: $discriminator")

Performance Notes

See Also


← Codec Overview | API | Type Rules | Examples | RLP Comparison