borer-compat-circe

The borer-compat-circe module allows existing serialization code written against circe to be re-used for CBOR (de)serialization with minimal effort.

Background

circe is a mature JSON library that’s quite popular throughout the Scala ecosystem.
Contrary to borer circe (de)serializes JSON not directly to/from the application-level model classes but models JSON documents with an intermediate-level AST (Abstract Syntax Tree) or DOM (Document Object Model).
As shown in Figure 1 the Encoder / Decoder logic you write (or that circe derives for you) merely translates between your own types and this JSON AST/DOM.

circe schematic
Figure 1. circe schematic

borer on the other hand translates directly between your application-level model classes and the JSON document, without going through an intermediate AST/DOM representation (Figure 2).

borer schematic
Figure 2. borer schematic

The borer-compat-circe module provides you with borer Encoder and Decoder type classes for circe’s AST node types, which allows you to combine both libraries as shown in Figure 3.

borer-circe-compat schematic
Figure 3. borer-circe-compat schematic

The benefit of this construct is that existing encoding/decoding logic that so far has been only targeting JSON via circe can now also be used to target CBOR through borer.
(Theoretically you could also use borer to target JSON with this construct but there wouldn’t be much point in doing so as circe can of course read and write its own AST nodes to and from JSON without borer’s help. Also, due to optimal integration between the layers, circe can likely do the job more efficiently that any external library ever could.)

Usage

When you include the borer-compat-circe module as a dependency (see the Getting Started chapter for details) you can write code such as this:

sourceimport io.circe.{Decoder, Encoder} // NOTE: circe (!) Encoders / Decoders
import io.bullet.borer.Cbor
import io.bullet.borer.compat.circe.* // the borer codec for the circe AST

// serializes a value to CBOR given that a circe `Encoder` is available
def serializeToCbor[T: Encoder](value: T): Array[Byte] =
  Cbor.encode(value).toByteArray

// serializes a value from CBOR given that a circe `Decoder` is available
def deserializeFromCbor[T: Decoder](bytes: Array[Byte]): T =
  Cbor.decode(bytes).to[T].value

val value = List("foo", "bar", "baz") // example value

val bytes = serializeToCbor(value)
bytes ==> hex"8363666f6f636261726362617a"

deserializeFromCbor[List[String]](bytes) ==> value

Limitations

Since JSON is merely a subset of CBOR and, as such, there are constructs in CBOR that do not directly map onto JSON not all CBOR documents can be easily decoded via a JSON deserialization layer such as the one provided by circe.

Most importantly the following CBOR constructs do not readily and easily map onto JSON:

undefined
By default undefined values are decoded as null values.
Raw Byte Strings
By default raw byte strings are base64-encoded and passed to circe as JSON strings.
Custom Simple Values
By default an exception is thrown upon reading a custom CBOR “simple value”.

The behavior of the borer-circe-compat decoding logic can be customized, if necessary, by constructing the borer Decoder[io.circe.Json] with a custom call to io.bullet.borer.compat.circe.circeJsonAstDecoder(...).

See the module sources for full details.