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
Unit
: Empty byte sequenceByte
: Single byteLong
: 8-byte big-endianInstant
: Epoch milliseconds as Long
Numeric Types
BigInt
: Sign-aware variable-length encodingBigNat
: Natural numbers with variable-length encoding
Collections
List[A]
: Size prefix + ordered elementsOption[A]
: Encoded as zero or one-element listSet[A]
: Lexicographically sorted after encodingMap[K, V]
: Treated asSet[(K, V)]
Product Types
Tuple2[A, B]
,Tuple3[A, B, C]
, etc.: Automatic derivation- Case classes: Automatic derivation via
Mirror.ProductOf
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:
- Insufficient bytes: Not enough data to decode required type
- Invalid format: Data doesn't match expected structure
- Validation failure: Value decoded successfully but failed validation (from
emap
)
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
- Encoding: O(n) where n is the size of data structure
- Decoding: O(n) with early exit on failures
- Product derivation: Compile-time, no runtime overhead
- Collection sorting: O(k log k) for Sets/Maps where k is collection size
See Also
- Type Rules: Detailed encoding specifications for each type
- Examples: Real-world usage patterns
- RLP Comparison: Differences from Ethereum RLP
← Codec Overview | API | Type Rules | Examples | RLP Comparison