Xenomorph is a Scala library for building well-typed descriptions of other
Scala data structures, from which one can then automatically derive
serialization, parsing, and generation functions. Implementations are
currently provided for producing argonaut DecodeJson
and EncodeJson instances, as well as scalacheck
Gen values. Similar facilities for scodec, protobuf, and whatever else
might be useful will be coming as time permits and interest demands.
Xenomorph is still awaiting an initial release, so for now you'll have to build it locally for yourself.
Begin with the data type for which you wish to create a schema. Here's an example:
import monocle.macros._
@Lenses case class Person(
name: String,
birthDate: Instant,
roles: Vector[Role]
)
sealed trait Role
case object User extends Role {
val prism = GenPrism[Role, User.type]
}
@Lenses case class Administrator(department: String, subordinateCount: Int) extends Role
object Administrator {
val prism = GenPrism[Role, Administrator]
}In this example, you can see:
- A simple record type
Person - A sum type
Rolewith two constructors,UserandAdministrator
To build a schema for the Person type, we will use the Scalaz applicative
constructor at arity 3 (^^) as shown below:
import scalaz.syntax.apply._
import xenomorph.Schema._
import xenomorph.json.JType._
val personSchema: Schema[JSchema, Person] = rec(
^^(
required("name", jStr, Person.name.asGetter),
required(
"birthDate", jLong.composeIso(Iso(new Instant(_:Long))((_:Instant).getMillis)),
Person.birthDate.asGetter
),
required("roles", jArray(roleSchema), Person.roles.asGetter)
)(Person.apply _)
)The schema for the Role sum type is created as a nonempty list of
alternatives, each of which provides a prism from the sum type to the
underlying data type associated with each constructor. In the case of the
User case object, the underlying schema is that of the empty object, which is
isomorphic to Unit. () is the empty tuple, so we treat the empty record as
the Unit schema constructor.
val roleSchema: Schema[JSchema, Role] = Schema.oneOf(
alt[JSchema, Role, User.type](
"user",
Schema.const(User),
User.prism
) ::
alt[JSchema, Role, Administrator](
"administrator",
rec(
^(
required("department", jStr, Administrator.department.asGetter),
required("subordinateCount", jInt, Administrator.subordinateCount.asGetter)
)(Administrator.apply _)
),
Administrator.prism
) :: shapeless.HNil
)This schema is constructed using the JType GADT to define the set of recognized
primitive types. However, the set of primitive types is a user-definable feature at
the time of schema construction.
Once you have a Schema value, you can use it to produce parsers, serializers, and generators.
import argonaut._
import xenomorph.json.ToJson._
import xenomorph.json.FromJson._
import xenomorph.scalacheck.ToGen._
val personJson: Json = personSchema.toJson(person)
val parsedPerson: Option[Person] = personSchema.fromJson(personJson).toOption
val personGen: Gen[Person] = personSchema.toGenKris Nuttycombe (@nuttycom) Antonio Alonso Dominguez Doug Clinton