Skip to content

Instantly share code, notes, and snippets.

@harpocrates
Created June 27, 2019 14:32
Show Gist options
  • Save harpocrates/e5b5baf072551572203231f126f0f2f6 to your computer and use it in GitHub Desktop.
Save harpocrates/e5b5baf072551572203231f126f0f2f6 to your computer and use it in GitHub Desktop.
Quick example demonstrating custom type-safe serialization with typeclasses and generic programming
import scala.reflect.ClassTag
import shapeless._
import shapeless.labelled._
/*
* Simple Json type
*/
sealed trait Json
object Json {
def apply[T](implicit ser: JsSerialization[T]) = ser
def serialize[T](t: T)(implicit ser: JsSerialization[T]) = ser.toJson(t)
def deserialize[T](j: Json)(implicit ser: JsSerialization[T]) = ser.fromJson(j)
}
case class JsObject(map: Map[Symbol, Json]) extends Json {
override def toString =
map.toList
.map { case (k,v) => " \"" + k.name + "\": " + v }
.mkString("{", ",", " }")
}
case class JsNumber(num: Double) extends Json {
override def toString = num.toString
}
case class JsString(str: String) extends Json {
override def toString = "\"" + str + "\""
}
case class JsArray(arr: Array[Json]) extends Json {
override def toString = arr.toList.mkString("[ ", ", ", " ]")
}
case object JsNull extends Json {
override def toString = "null"
}
/*
* JSON serialization interface
*/
trait JsSerialization[T] {
def toJson(t: T): Json
def fromJson(json: Json): T
}
object JsSerialization extends SimpleImpls with GenericImpls
case class JsError(msg: String) extends Exception(msg)
/*
* Serializing basic JSON types
*/
trait SimpleImpls {
// Serialize doubles
implicit val doubleImpl = new JsSerialization[Double] {
def toJson(d: Double) = JsNumber(d)
def fromJson(json: Json) = json match {
case JsNumber(double) => double
case other => throw JsError(s"Cannot read Double from $other")
}
}
// Serialialize strings
implicit val stringImpl = new JsSerialization[String] {
def toJson(s: String) = JsString(s)
def fromJson(json: Json) = json match {
case JsString(string) => string
case other => throw JsError(s"Cannot read String from $other")
}
}
// Serialize arrays
implicit def arrayImpl[T](implicit
tImpl: Lazy[JsSerialization[T]],
classTag: ClassTag[T]
) = new JsSerialization[Array[T]] {
def toJson(a: Array[T]) = JsArray(a.map(t => tImpl.value.toJson(t)))
def fromJson(json: Json): Array[T] = json match {
case JsArray(array) => array.map(tImpl.value.fromJson)
case other => throw JsError(s"Cannot read Array from $other")
}
}
// Serialize options
implicit def optionImpl[T](implicit
tImpl: Lazy[JsSerialization[T]]
) = new JsSerialization[Option[T]] {
def toJson(o: Option[T]) = o.fold[Json](JsNull)(tImpl.value.toJson)
def fromJson(json: Json): Option[T] = json match {
case JsNull => None
case other => Some(tImpl.value.fromJson(other))
}
}
}
/*
* Generic JSON impls
*/
trait GenericImpls {
implicit def genericImpl[T, Repr <: HList](implicit
lgen: LabelledGeneric.Aux[T, Repr],
gimpl: GenericJsSerialization[Repr]
) = new JsSerialization[T] {
def toJson(t: T): Json = JsObject(gimpl.toJsonMap(lgen.to(t)))
def fromJson(json: Json): T = json match {
case JsObject(map) => lgen.from(gimpl.fromJsonMap(map))
case other => throw JsError(s"Cannot read generic object from $other")
}
}
sealed trait GenericJsSerialization[T <: HList] {
def toJsonMap(t: T): Map[Symbol, Json]
def fromJsonMap(obj: Map[Symbol, Json]): T
}
implicit val hnilImpl = new GenericJsSerialization[HNil] {
def toJsonMap(h: HNil) = Map.empty
def fromJsonMap(obj: Map[Symbol, Json]) = if (obj.nonEmpty) {
throw JsError(s"Leftover fields ${JsObject(obj)}")
} else {
HNil
}
}
implicit def hconsImpl[Name <: Symbol, Value, T <: HList](implicit
keyName: Witness.Aux[Name],
headImpl: Lazy[JsSerialization[Value]],
tailImpl: Lazy[GenericJsSerialization[T]]
) = new GenericJsSerialization[FieldType[Name, Value] :: T] {
def toJsonMap(h: FieldType[Name, Value] :: T) = {
val newEntry = (keyName.value -> headImpl.value.toJson(h.head))
tailImpl.value.toJsonMap(h.tail) + newEntry
}
def fromJsonMap(obj: Map[Symbol, Json]) = {
val fieldJson = obj.getOrElse(
keyName.value,
throw JsError(s"Cannot find property ${keyName.value} in object $obj")
)
val head = headImpl.value.fromJson(fieldJson)
val tail = tailImpl.value.fromJsonMap(obj - keyName.value)
field[Name](head) :: tail
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment