meteor Codec
meteor
provides Encoder
and Decoder
type classes to convert any A
to and from Java’s
Attributevalue
. Their signatures are followed:
trait Encoder[A] {
def write(a: A): AttributeValue
}
trait Decoder[A] {
def read(av: AttributeValue): Either[DecoderError, A]
}
Please note that the write side is always success, such that, given an A
, we can always write it
to an AttributeValue
. However, reading an AttributeValue
into A
might fail as we don’t know
the schema of an AttributeValue
.
If you need to create both Encoder
and Decoder
, there is a convenient type class Codec
which
extends both Encoder
and Decoder
. Codec for value class can be instantiated using Codec.iso
:
case class Id(value: String) extends AnyVal
implicit val idCodec: Codec[Id] = Codec.iso[String, Id](Id.apply)(_.value)
Instances
The library provides built-in instances for primitive and common types. For Option[T]
, there is an
Encoder[Option[T]]
but there is no Decoder[Option[T]]
. The reason is that None
can be
written to an AttributeValue
of null
, however, because anything can be null
in Java, we can’t
make the assumption to read null
as None
. For example:
case class Book(id: Int, content: String)
case class Exam(allow: Option[Book])
val exam1 = Exam(Some(null))
val exam2 = Exam(None)
User might choose to handle Encoder[Book]
such that val book: Book = null
is written as an
AttributeValue
of null
. Hence, if there were a Decoder[Option[T]]
, exam1
would be read as
exam2
which changed the equality of the original value. It is always recommended to use Scala’s
Option
over null
. To help with this issue, the library provides some syntax helpers when reading
Option
. Alternatively, you can use Dynosaur codecs where it
enforces a single schema for both write and read.
Syntax
The following syntax helpers are provided to make conversion to and from AttributeValue
easier.
Encoder Syntax
Convert a primitive value to AttributeValue
of equivalent type:
import meteor.syntax._
// from a primitive value to AttributeValue
val intAsAv: AttributeValue = 1.asAttributeValue
Convert case class instance to AttributeValue
as a Map
:
// from a case class to AttributeValue
val bookAsAv: AttributeValue = Map(
"id" -> book.id.asAttributeValue,
"content" -> book.content.asAttributeValue
).asAttributeValue
val examAsAv: AttributeValue = Map(
"allow" -> exam.allow.asAttributeValue // require an Encoder[Book] in scope
).asAttributeValue
Decoder Syntax
Attempt converting AttributeValue
to a value of given type:
import meteor.errors.DecoderError
import meteor.syntax._
val int: Either[DecoderError, Int] = intAsAv.as[Int]
val optInt: Either[DecoderError, Option[Int]] = intAsAv.asOpt[Int]
val bookId: Either[DecoderError, Int] = bookAsAv.getAs[Int]("id")
val optBook: Either[DecoderError, Option[Book]] = examAsAv.getOpt[Book]("allow")
Note that there are dedicated methods: asOpt
and getOpt
to help decoding optional values when
we want to force the behaviour of reading an AttributeValue
of null
or when a field is missing
to Scala’s None
.
Auto-derivation
Currently, there is no plan to support auto derivation for codec. The reasons are:
- Auto derivation hides how data is being written and can lead to surprises. Let’s take
Scanamo
’s auto derivation as an example:
import org.scanamo._
import org.scanamo.semiauto._
case class Id(value: Int) extends AnyVal
case class Book(id: Id)
implicit val idFormat: DynamoFormat[Id] = deriveDynamoFormat[Id]
implicit val bookFormat: DynamoFormat[Book] = deriveDynamoFormat[Book]
val book1 = Book(Id(1))
book1
is being written into DynamoDB JSON as:
{
"M": {
"id": {
"M": {
"value": {
"N": 1
}
}
}
}
}
whereas it can be as simple as:
{
"M": {
"id": {
"N": 1
}
}
}
- When using auto derivation, conversion is usually required to map between different layers of model. With the explicit codec approach, we need fewer layers of model but conversion gets more boilerplate. I think the problems are the same with 2 approaches, they just being solved at different places.