circe Validation

GitLab CI GitLab CI Maven Central License


libraryDependencies += "io.taig" %%% "circe-validation" % "0.3.1"


This repository is primarily an experimental exploration of validation handling with circe. My main concerns about the current implementation are:

  • ValidatingDecoder lives in io.circe to override the package scoped method decodeAccumulating
  • Validation failures cannot be distinguished from circe‚Äôs DecodingFailures


import cats.implicits._
import io.circe.generic.semiauto._
import io.circe.parser._
import io.circe.Decoder
import io.circe.ValidatingDecoder

object Validation {
  def email(value: String): ValidatedNel[String, String] =
    if (value.contains("@")) valid(value)
    else invalidNel("not an email")

  def min(length: Int)(value: String): ValidatedNel[String, String] =
    if (value.length < length) invalidNel(s"min $length")
    else valid(value)

case class Name(value: String)
case class Email(value: String)
case class Person(name: Name, email: Email)

def liftName(value: String): ValidatedNel[String, Name] =
  Validation.min(3)(value) map Name

def liftEmail(value: String): ValidatedNel[String, Email] = |+| Validation.min(5)(value) map Email

implicit val decoderName: Decoder[Name] = ValidatingDecoder[String](liftName)
implicit val decoderEmail: Decoder[Email] = ValidatingDecoder[String](liftEmail)
implicit val decoderPerson: Decoder[Person] = deriveDecoder

val text = """{ "name":"Qt", "email":"foo" }"""
val Right(json) = parse(text)
val cursor = json.hcursor

// Default behavior is decoding without accumulation
val Left(decodingFailure) = Decoder[Person].apply(cursor)
val Invalid(accumulatedDecodingFailures) = Decoder[Person].decodeAccumulating(cursor)
// res0: String = "DecodingFailure at .name: min 3"
// res1: String = "NonEmptyList(DecodingFailure at .name: min 3, DecodingFailure at .email: not an email, DecodingFailure at .email: min 5)"