Setting: We serve a GraphQl API using Sangria and Akka HTTP.

We’ll go through the steps necessary to parse value classes.

This is the model for our API:

case class Color(underlying: String) extends AnyVal
case class Cat(name: String, color: Color)

Our nice API is completely stateless. We just provide a function to paint an input cat in a new color.

object MutationSchema {
  def mutationType: ObjectType[Unit, Unit] = ObjectType(
    "Mutation",
    fields[Unit, Unit](
      Field(
        "paintCat",
        Cat.gqlType,
        arguments = List(
          Argument("cat", Cat.gqlInputType),
          Argument("color", Color.gqlType)
        ),
        resolve = c => paintCat(c.arg[Cat]("cat"), c.arg[Color]("color"))
      )
    )
  )

  def paintCat(cat: Cat, color: Color): Cat =
    cat.copy(color = color)
}

We need the graphQl types. For our value class Color, we can use ScalarAlias to make it exactly like a known gqlType (String). We just need to provide functions to encode and decode.

object Color {
  implicit val gqlType: ScalarAlias[Color, String] =
    ScalarAlias[Color, String](StringType, _.underlying, c => Right(Color(c)))
}

For Cat we can use deriveObjectType to derive the gqlType from the case class. It will use the implicit gqlType for Color that we provided in the previous step.
Note that it needs both an ObjectType (for output) and an InputObjectType (for input). Be careful here that the type names are unique. Strange things will go wrong if they’re not.

object Cat {
    implicit val gqlType: ObjectType[Unit, Cat] = deriveObjectType[Unit, Cat](ObjectTypeName("Cat"))
    implicit val gqlInputType: InputObjectType[Cat] = deriveInputObjectType[Cat](InputObjectTypeName("CatInput"))
}

For complete code check out github.
Now it compiles and runs. But when we try to paint a cat, disaster strikes!

{
  "data": null,
  "errors": [
    {
      "message": "Argument 'cat' has invalid value: Attempt to decode value on failed cursor: DownField(underlying),DownField(color) (line 5, column 17):\n  paintCat(cat: $cat, color: $color, speed: $speed) {\n                ^",
      "path": [
        "paintCat"
      ],
      "locations": [
        {
          "line": 5,
          "column": 17
        }
      ]
    }
  ]
}

It’s still looking for the underlying field under Color.
To fix this, we also need to add a Circe decoder.

  implicit val circeDecoder: Decoder[Color] = (c: HCursor) => c.as[String].map(Color.apply)

Now we can happily paint our cats! I used Insomnia as a frontend and copied the relevant parts:

mutation paintCat(
  $cat: CatInput!,
  $color: String!) {
  paintCat(cat: $cat, color: $color) {
    name
    color
  }
}

{
  "cat": {
    "name": "Koffie",
    "color": "grey",
  },
  "color": "black"
}

{
  "data": {
    "paintCat": {
      "name": "Koffie",
      "color": "black"
    }
  }
}

Still we can do even better. As any cat lover knows, color is not the only thing to improve, also speed.
Speed is derived from Int. We add an extra requirement that it needs to be positive. Also we give it a name and description.

case class Speed(underlying: Int) extends AnyVal
object Speed {
  def fromInt(i: Int): Speed = {
    require(i > 0, "cats only go forwards!")
    Speed(i)
  }

  implicit val circeDecoder: Decoder[Speed] = (c: HCursor) => c.as[Int].map(fromInt)
  implicit val gqlType: ScalarAlias[Speed, Int] = {
    val renamed = IntType.copy(name = "Speed", description = Some("Speed in m/s"))
    ScalarAlias(renamed, _.underlying, parse)
  }

  private def parse(input: Int): Either[SpeedParseViolation, Speed] =
    Try(fromInt(input)) match {
      case Success(t) => Right(t)
      case Failure(e) => Left(new SpeedParseViolation(s"error parsing speed: ${e.getMessage}"))
    }

  private class SpeedParseViolation(val errorMessage: String) extends Violation
}

In the schema documentation we can now see that color is just a String, but speed got it’s own type. This can be very convenient if you also want more precise typing in the frontend.

Cat:
no description

color: String!
name: String!
speed: Speed!
Speed:
Speed in m/s

Now our speed field has Int parsing that we got from IntType. This is what we get for value "fast":

Variable '$speed' expected value of type 'Speed!' but got: "fast". Reason: Int value expected

And also returns our requirements. This happens with a negative value:

Field '$speed' has wrong value: error parsing speed: requirement failed: cats only go forwards!.

We can simplify this a bit more by taking out the common parts.

object SangriaUtils {

  /** Needed at runtime to correctly decode value classes.
    * @tparam T The internal value class
    * @tparam U The external class that we use for in/output
    * @param decode function to decode input U into internal type T
    * @return The Circe decoder that we should add as an implicit
    */
  def circeValueClassDecoder[T, U: Decoder](decode: U => T): Decoder[T] = (c: HCursor) => c.as[U].map(decode)

  /** Create a gql type for value class `T`, that will be an alias for an existing gqlType `U`.
    * It will still be represented as `U` by graphQl.
    * @tparam T The internal value class
    * @tparam U The external class that we use for in/output
    * @param decode function to decode input U into internal type T
    * @param encode function to encode internal type T to output U
    * @param cd to ensure there is also a circe decoder for T
    * @return The GraphQl type that we should add as an implicit
    */
  def gqlAliasType[T: ClassTag, U](
      decode: U => T,
      encode: T => U
  )(implicit cd: Decoder[T], uType: ScalarType[U]): ScalarAlias[T, U] =
    ScalarAlias[T, U](uType, encode, gqlParse(decode))

  /** Parse input of gqlType `U` into a value class `T`.
    * It will be represented as `className[T]` by graphQl.
   * @tparam T The internal value class
   * @tparam U The existing GraphQl type to base this on
   * @param decode function to decode input U into internal type T
   * @param encode function to encode internal type T to output U
   * @param description graphQl documentation for T
   * @param cd to ensure there is also a circe decoder for T
   * @return The GraphQl type that we should add as an implicit
    */
  def gqlValueClassType[T: ClassTag, U](
      decode: U => T,
      encode: T => U,
      description: String
  )(implicit cd: Decoder[T], uType: ScalarType[U]): ScalarAlias[T, U] = {
    val renamed = uType.copy(name = className[T], description = Some(description))
    ScalarAlias[T, U](renamed, encode, gqlParse(decode))
  }

  private def gqlParse[T: ClassTag, U](decode: U => T)(input: U): Either[GqlParseViolation[T], T] =
    Try(decode(input)) match {
      case Success(t) => Right(t)
      case Failure(_) => Left(new GqlParseViolation[T])
    }

  private class GqlParseViolation[T: ClassTag] extends Violation {
    override def errorMessage: String = s"Error while parsing ${className[T]}"
  }

  //Include the package to make sure names are unique
  private def className[T: ClassTag]: String = classTag[T].runtimeClass.getName.replace(".", "_").replace("$", "_")
}

Which we can then use in our value classes:

case class Color(underlying: String) extends AnyVal
object Color {
  implicit val circeDecoder: Decoder[Color] = SangriaUtils.circeValueClassDecoder(apply)
  implicit val gqlType: ScalarAlias[Color, String] = SangriaUtils.gqlAliasType(apply, _.underlying)
}

case class Speed(underlying: Int) extends AnyVal
object Speed {
  def fromInt(i: Int): Speed = {
    require(i > 0, "cats only go forwards!")
    Speed(i)
  }

  implicit val circeDecoder: Decoder[Speed] = SangriaUtils.circeValueClassDecoder(fromInt)
  implicit val gqlType: ScalarAlias[Speed, Int] = SangriaUtils.gqlValueClassType[Speed, Int](fromInt, _.underlying, "Speed in m/s")
}
shadow-left