UBJson Derivation
Original module -- derives UBJsonValueCodec for case classes, sealed traits, Scala 3 enums, Java enums, and more. UBJson is a binary JSON format with the same data model as JSON but more compact wire representation.
Installation
sbt
Cross-platform (JVM / Scala.js / Scala Native):
Quick start
Encoding and decoding a case class
//> using dep com.kubuszok::kindlings-ubjson-derivation:0.2.0
import hearth.kindlings.ubjsonderivation._
case class Person(name: String, age: Int)
val codec: UBJsonValueCodec[Person] = UBJsonValueCodec.derived[Person]
// Encode to UBJson binary
val writer = new UBJsonWriter()
codec.encode(writer, Person("Alice", 30))
val bytes: Array[Byte] = writer.toByteArray
// Decode back
val decoded: Person = codec.decode(new UBJsonReader(bytes))
println(decoded)
// expected output:
// Person(Alice,30)
API
Derivation methods
| Method | Returns | Description |
|---|---|---|
UBJsonValueCodec.derived[A] |
UBJsonValueCodec[A] |
Semi-automatic codec |
UBJsonValueCodec.derived[A] |
UBJsonValueCodec[A] |
Sanely-automatic (given/implicit) |
All methods take an implicit/using UBJsonConfig parameter (defaults to UBJsonConfig.default).
Type class interface
UBJsonValueCodec[A] provides three members:
| Member | Signature | Description |
|---|---|---|
decode |
(reader: UBJsonReader): A |
Decode a value from a UBJson reader |
encode |
(writer: UBJsonWriter, value: A): Unit |
Encode a value to a UBJson writer |
nullValue |
A |
The null/default value for type A |
Extension methods
Import UBJsonValueCodecExtensions._ for extra combinators on codec instances:
| Method | Description |
|---|---|
codec.map[B](f)(g) |
Transform decoded values with f, encoded values with g |
codec.mapDecode[B](f)(g) |
Like map but f returns Either[String, B] for validation |
Configuration
import hearth.kindlings.ubjsonderivation._
implicit val config: UBJsonConfig = UBJsonConfig.default
.withSnakeCaseFieldNames
.withDiscriminator("type")
.withTransientNone
| Builder method | Description |
|---|---|
withSnakeCaseFieldNames |
fieldName -> field_name |
withKebabCaseFieldNames |
fieldName -> field-name |
withPascalCaseFieldNames |
fieldName -> FieldName |
withScreamingSnakeCaseFieldNames |
fieldName -> FIELD_NAME |
withFieldNameMapper(f) |
Custom field name transform |
withSnakeCaseAdtLeafClassNames |
ADT subtype name -> snake_case |
withKebabCaseAdtLeafClassNames |
ADT subtype name -> kebab-case |
withAdtLeafClassNameMapper(f) |
Custom ADT subtype name transform |
withDiscriminator(field) |
ADT discriminator field name |
withSkipUnexpectedFields(skip) |
Skip unexpected fields during decoding |
withEnumAsStrings |
Encode Scala 3 / Java enums as strings |
withTransientDefault |
Skip fields with default values during encoding |
withTransientEmpty |
Skip empty collections during encoding |
withTransientNone |
Skip None fields during encoding |
withRequireCollectionFields |
Fail if collection field is missing |
withRequireDefaultFields |
Fail if field with default is missing |
withCheckFieldDuplication |
Fail on duplicate field names |
withBigDecimalPrecision(n) |
Max BigDecimal precision (default: 34) |
withBigDecimalScaleLimit(n) |
Max BigDecimal scale (default: 6178) |
withBigDecimalDigitsLimit(n) |
Max BigDecimal digits (default: 308) |
withMapMaxInsertNumber(n) |
Max map entries (DoS protection) |
withSetMaxInsertNumber(n) |
Max set entries (DoS protection) |
Annotations
| Annotation | Package | Description |
|---|---|---|
@fieldName("name") |
hearth.kindlings.ubjsonderivation.annotations |
Override the UBJson field name |
@transientField |
hearth.kindlings.ubjsonderivation.annotations |
Exclude field from codec (must have default) |
@stringified |
hearth.kindlings.ubjsonderivation.annotations |
Encode numeric/boolean field as a string |
import hearth.kindlings.ubjsonderivation.annotations._
case class User(
@fieldName("user_name") name: String,
@transientField cache: Option[String] = None
)
Usage examples
Sealed trait with discriminator
//> using dep com.kubuszok::kindlings-ubjson-derivation:0.2.0
import hearth.kindlings.ubjsonderivation._
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
implicit val config: UBJsonConfig = UBJsonConfig.default
.withDiscriminator("type")
val codec: UBJsonValueCodec[Shape] = UBJsonValueCodec.derived[Shape]
val writer = new UBJsonWriter()
codec.encode(writer, Circle(5.0): Shape)
val decoded = codec.decode(new UBJsonReader(writer.toByteArray))
println(decoded)
// expected output:
// Circle(5.0)
Transient fields and defaults
//> using dep com.kubuszok::kindlings-ubjson-derivation:0.2.0
import hearth.kindlings.ubjsonderivation._
implicit val config: UBJsonConfig = UBJsonConfig.default
.withTransientDefault
.withTransientNone
case class Settings(
host: String,
port: Int = 8080,
debug: Option[Boolean] = None
)
val codec: UBJsonValueCodec[Settings] = UBJsonValueCodec.derived[Settings]
// Default and None fields are omitted during encoding
val writer = new UBJsonWriter()
codec.encode(writer, Settings("localhost"))
// Missing fields use defaults during decoding
val decoded = codec.decode(new UBJsonReader(writer.toByteArray))
println(decoded)
// expected output:
// Settings(localhost,8080,None)
Recursive data types
//> using dep com.kubuszok::kindlings-ubjson-derivation:0.2.0
import hearth.kindlings.ubjsonderivation._
case class TreeNode(value: Int, children: List[TreeNode])
// Recursive types just work -- no special setup needed
val codec: UBJsonValueCodec[TreeNode] = UBJsonValueCodec.derived[TreeNode]
val tree = TreeNode(1, List(TreeNode(2, Nil), TreeNode(3, List(TreeNode(4, Nil)))))
val writer = new UBJsonWriter()
codec.encode(writer, tree)
val decoded = codec.decode(new UBJsonReader(writer.toByteArray))
println(decoded)
// expected output:
// TreeNode(1,List(TreeNode(2,List()), TreeNode(3,List(TreeNode(4,List())))))
Field name mapping
//> using dep com.kubuszok::kindlings-ubjson-derivation:0.2.0
import hearth.kindlings.ubjsonderivation._
implicit val config: UBJsonConfig = UBJsonConfig.default
.withSnakeCaseFieldNames
case class UserProfile(firstName: String, lastName: String, emailAddress: String)
// Fields encoded as: first_name, last_name, email_address
val codec: UBJsonValueCodec[UserProfile] = UBJsonValueCodec.derived[UserProfile]
val writer = new UBJsonWriter()
codec.encode(writer, UserProfile("Alice", "Smith", "alice@example.com"))
val decoded = codec.decode(new UBJsonReader(writer.toByteArray))
println(decoded)
// expected output:
// UserProfile(Alice,Smith,alice@example.com)
Debugging
Import the debug package to log the generated codec during compilation:
Or enable project-wide via scalac option: