commons-core module comes with built-in implementation of Concise Binary Object Representation via
CborInput and CborOutput. They can be used to serialize and deserialize any type that has a GenCodec instance.
However, plain GenCodec cannot use all the power of CBOR, requiring some additional support.
Nothing else than a GenCodec instance is necessary to serialize a type into CBOR. For example:
import com.avsystem.commons.serialization._
import com.avsystem.commons.serialization.cbor._
case class Thing(int: Int, str: String)
object Thing extends HasGenCodec[Thing]
val cborBytes: Array[Byte] = CborOutput.write(Thing(42, "foo"))You can also serialize into RawCbor, a wrapper over Array[Byte] that provides sensible equals, hashCode,
toString and other utilities.
val rawCbor: RawCbor = CborOutput.writeRawCbor(Thing(42, "foo"))RawCbor can also be used as a field type in a case class in order to represent arbitrary CBOR-serialized data:
case class RawCborThing(id: Int, data: RawCbor)
object RawCborThing extends HasGenCodec[RawCborThing]CBOR lists and maps can be encoded in two ways:
- with explicit number of elements at the beginning (more compact, allows preallocation of buffers and better performance)
- with unspecified number of elements at the beginning along with a break byte (
0xFF) at the end (better for streaming applications)
When serializing collections into CBOR, you can control whether they are explicitly sized. This is done with
SizePolicy that can be passed into CborOutput, e.g.
val cborList = CborOutput.write(List(1, 2, 3), sizePolicy = SizePolicy.Required)SizePolicy has three values:
Required- Collections are always written with explicit size in their CBOR representation. This may be slow for collections that don't have a fast.sizemethod, e.g.ListOptional- Only collections that have a fast.sizemethod are written with explicit size, e.g.Vector. This is the default behavior.Ignored- All collections are written without explicit size
GenCodec has a strong assumption that all object/map keys are Strings, like in JSON. One of the consequences is that
serializing a Map[K, V] with GenCodec requires an instance of GenKeyCodec for K so that keys can be converted to
strings.
However, CBOR has no such limitations. Map keys can be arbitrary CBOR values. There are some additional tools
in GenCodec and CborInput/Output that can leverage this.
CborInput.read and CborOutput.write accept a CborKeyCodec parameter that defines translation between string-typed
keys and arbitrary CBOR-encoded keys. You can use this e.g. to give a numeric label to every textual field, reducing the
size of resulting CBOR.
val keyCodec = new CborKeyCodec {
def writeFieldKey(fieldName: String, output: CborOutput): Unit = fieldName match {
case "intField" => output.writeInt(1)
case "strField" => output.writeInt(2)
case name => output.writeString(name)
}
def readFieldKey(input: CborInput): String = input.readInitialByte().majorType match {
case MajorType.Unsigned => input.readInt() match {
case 1 => "intField"
case 2 => "strField"
case n => throw new ReadFailure(s"unknown CBOR field label: $n")
}
case _ => input.readString()
}
}
case class Thing(intField: Int, strField: String)
object Thing extends HasGenCodec[Thing]
val cborBytes: Array[Byte] = CborOutput.write(Thing(42, "foo"), keyCodec)This solution is nice when field names are often reused across multiple case classes that end up being serialized into final CBOR. Other than that, this is somewhat clunky and does not give you precise control on a level of each case class.
In place of HasGenCodec, you can use HasCborCodec. It generates an instance of GenCodec that is optimized for
CBOR, which means two things:
- if your case class contains a field typed as
Map[K, V], map keys will be serialized directly into CBOR rather than converted into strings withGenKeyCodec(which would happen for standardGenCodec). - you can use
@cborKeyand@cborDiscriminatorannotations in your case class & sealed hierarchy definitions
Example:
@cborDiscriminator(0) sealed trait UnionData
object UnionData extends HasCborCodec[UnionData] {
@cborKey(1) case class Textual(@cborKey(1) txt: String) extends UnionData
@cborKey(2) case class Numeric(@cborKey(true) num: Int) extends UnionData
@cborKey(3) case class Mapping(@cborKey("m") map: Map[Int, String]) extends UnionData
}Here are some examples of how UnionData serializes into CBOR (using diagnostic notation):
UnionData.Textual("foo") => {0: 1, 1: "foo"}
UnionData.Numeric(42) => {0: 2, true: 42}
UnionData.Mapping(Map(1 -> "one", 2 -> "two")) => {0: 3, "m": {1: "one", 2: "two"}}
- the field with key
0in each case class representation is the discriminator specified with@cborDiscriminatorannotation - discriminator values (
1,2and3) come from@cborKeyannotations applied on each case class - case class field keys (
1,trueand"m") come from@cborKeyannotations applied on case class fields Map[Int, String]has keys serialized as plain numbers rather than strings
NOTE: @cborKey and @cborDiscriminator annotations accept any value serializable with GenCodec