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.
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).
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.
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 asnull
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.