JSON Specifics
Since the CBOR data item primitives are a super-set of what is available in JSON, or, said differently, everything in JSON has a counterpart in CBOR, it’s not hard for borer to also support encoding to and decoding from JSON.
From borer’s point of view JSON is simply a slightly different binary format that only supports a subset of the CBOR data primitives. Like CBOR borer encodes and decodes JSON in a single pass, UTF-8 encoding and decoding to and from raw bytes on the fly.
All higher-level abstractions (i.e. Writer
, Reader
, Encoder
, Decoder
, Codec
, etc.) are essentially agnostic to the (de)serialization target format. Nevertheless, the Writer
and Reader
types do have a target
member, which enables custom logic to discriminate between the two variants, if required.
Since the underlying JSON renderer will throw exceptions on attempts to write data primitives that are not supported in JSON (like CBOR Tags, for example), this is sometimes necessary to efficiently support both formats.
For example, in order to write an empty array in the most efficient way to both CBOR and JSON using only the most low-level data items one would use this approach:
sourceimport io.bullet.borer.Writer
def writeEmptyArray(w: Writer): w.type =
if (w.writingJson) w.writeArrayStart().writeBreak()
else w.writeArrayHeader(0) // fixed-sized Arrays are not supported in JSON
But on a slightly higher level the Writer
also gives you a way to write arrays (and maps) without having distinguish between CBOR and JSON yourself:
sourceimport io.bullet.borer.Writer
def writeAsUnaryArray(w: Writer, s: String): w.type =
w.writeArrayOpen(1) // automatically chooses the most efficient
.writeString(s)
.writeArrayClose() // way to write an array of size one
As long as you rely on the somewhat higher-level parts of the Reader
and Writer
APIs or construct your (de)serialization purely logic from borer’s built-in or (and/or derived) Encoders and Decoders, your application will support both CBOR and JSON at the same time without any special casing whatsoever.
Base Encodings for Binary Data
One big drawback of JSON over CBOR is that JSON doesn’t provide any “first-class” representation of binary data. This is typically worked around by mapping binary data to a “Base Encoding”, e.g. the ones defined by RFC 4648.
In order to give your application an easy and flexible way to integrate with other systems borer supports a number of base encodings out of the box, specifically:
The default JSON encoding for Array[Byte]
is base64.
In order to switch to a different base encoding in a particular scope define the a pair of implicits as in this example:
sourceimport io.bullet.borer.{Decoder, Encoder, Json}
import io.bullet.borer.encodings.BaseEncoding
val binaryData = hex"DEADBEEF"
// Json.encode(binaryData).toByteArray or
Json.encode(binaryData).toUtf8String ==> """"3q2+7w==""""
{
// we need to explicitly define the encoder as well as the decoder
// in order to "override" the defaults for Array[Byte] on either side
given Encoder[Array[Byte]] = Encoder.forByteArray(BaseEncoding.zbase32)
given Decoder[Array[Byte]] = Decoder.forByteArray(BaseEncoding.zbase32)
Json.encode(binaryData).toUtf8String ==> """"54s575a""""
}
JSON Pretty Printing
Normally borer will output JSON in the most compact form, with no whitespace padding anywhere. However, you can enable “pretty” JSON rendering as shown here:
sourceimport io.bullet.borer.{Codec, Json}
import io.bullet.borer.derivation.MapBasedCodecs.*
sealed trait Animal derives Codec.All
case class Dog(age: Int, name: String) extends Animal
case class Cat(weight: Double, color: String, home: String) extends Animal
case class Mouse(colorRGB: List[Int], tail: Boolean) extends Animal
val animals = List(
Dog(5, "Rufus"),
Cat(4.7, "red", "next door"),
Mouse(colorRGB = List(73, 42, 64), tail = false),
)
Json
.encode(animals)
.withPrettyRendering(indent = 2) // enables pretty printing
.toUtf8String ==>
"""|[
| {
| "Dog": {
| "age": 5,
| "name": "Rufus"
| }
| },
| {
| "Cat": {
| "weight": 4.7,
| "color": "red",
| "home": "next door"
| }
| },
| {
| "Mouse": {
| "colorRGB": [
| 73,
| 42,
| 64
| ],
| "tail": false
| }
| }
|]""".stripMargin
When (not) to use borer for JSON
Since borer treats JSON as a binary format and reads/writes from/to raw bytes it isn’t optimized for consuming or producing Strings as input or output. (Strings have to first be UTF-8 encoded in order to be readable by borer.)
So, if you need to frequently consume String
input other JSON libraries will likely perform better. Also, if you need to manipulate the JSON structures in any way between (de)serializing from/to the wire and from/to your data model then borer will not help you and a DOM/AST-based JSON solution (like Circe) will likely be the better choice.
However, if all you need is an efficient way to convert raw network- or disk-bytes holding UTF-8 encoded JSON to and from your data model types, with no (or few) dependencies and maybe even with the option to target CBOR with no additional work required from your side, then borer should be a good choice.
Comparison with other Scala JSON Libraries
(Additions, corrections, improvement suggestions very welcome, especially to this section!)
- Circe
- AST/DOM- and type-class-based design
- very mature
- allows for extensive DOM-manipulation
- many integration option already available
- compatible with scala.js and [Scala Native]
- spray-json
- AST/DOM- and type-class-based design
- zero dependencies
- µPickle
- pull-style, type class-based design
- zero dependencies
- optional DOM
- also supports MessagePack
- compatible with scala.js
- Jackson Scala
- Java implementation with a Scala add-on
- very mature
- good performance
- Jsoniter Scala
- pull-style, type class-based design
- very low-level core API
- entirely macro-based high-level API
- no pre-defined AST/DOM
- no CBOR support