Dupin alternatives and similar packages
Based on the "Data Binding and Validation" category.
Alternatively, view Dupin alternatives based on common mentions on social networks and blogs.
CodeRabbit: AI Code Reviews for Developers
Do you think we are missing an alternative of Dupin or a related project?
README
Dupin
Dupin is a minimal, idiomatic, customizable validation for Scala.
Table of contents
Motivation
You may find Dupin useful if you want to...
- return something richer than
String
as validation message - use custom data kind for validation (
Future
,IO
, etc...) - reuse validation parts across whole project
Quick start
Add dupin dependency to the build file, let's assume you are using sbt:
libraryDependencies += Seq(
"com.github.yakivy" %% "dupin-core" % "0.1.4"
)
Describe the domain:
case class Name(value: String)
case class Member(name: Name, age: Int)
case class Team(name: Name, members: Seq[Member])
Define some validators:
import dupin.all._
import cats.implicits._
implicit val nameValidator = BaseValidator[Name]
.root(_.value.nonEmpty, _.path + " should be non empty")
implicit val memberValidator = BaseValidator[Member]
.combineP(_.name)(nameValidator)
.combinePR(_.age)(a => a > 18 && a < 40, _.path + " should be between 18 and 40")
implicit val teamValidator = BaseValidator[Team]
.combinePI(_.name)
.combineP(_.members)(element(memberValidator))
.combineR(_.members.size <= 8, _ => "team should be fed with two pizzas!")
Validate them all:
import dupin.all._
val validTeam = Team(
Name("Bears"),
List(
Member(Name("Yakiv"), 26),
Member(Name("Myroslav"), 31),
Member(Name("Andrii"), 25)
)
)
val invalidTeam = Team(
Name(""),
Member(Name(""), 0) :: (1 to 10).map(_ => Member(Name("valid name"), 20)).toList
)
val a = validTeam.validate.either
val b = validTeam.isValid
val c = invalidTeam.validate.list
assert(a == Right(validTeam))
assert(b)
assert(c == List(
".name should be non empty",
".members.[0].name should be non empty",
".members.[0].age should be between 18 and 40",
"team should be fed with two pizzas!"
))
Derivation
If you are a fan of value classes or self descriptive types, validators can be easily derived:
implicit val nameValidator = BaseValidator[Name]
.root(_.value.nonEmpty, _.path + " should be non empty")
implicit val ageValidator = BaseValidator[Int]
.root(a => a > 18 && a < 40, _.path + " should be between 18 and 40")
implicit val memberValidator = BaseValidator[Member].derive
Validation messages will look like:
val validMember = Member(Name("Yakiv"), 27)
val invalidMember = Member(Name(""), 0)
val validationResult = validMember.isValid
val messages = invalidMember.validate.list
assert(validationResult)
assert(messages == List(
".name should be non empty",
".age should be between 18 and 40",
))
Predefined validators
The more validators you have, the more logic can be reused without writing validators from the scratch. Let's write common validators for minimum and maximum Int
value:
import dupin.all._
def min(value: Int) = BaseValidator[Int].root(_ > value, _.path + " should be greater than " + value)
def max(value: Int) = BaseValidator[Int].root(_ < value, _.path + " should be less than " + value)
And since validators can be combined, you can create validators from other validators:
import dupin.all._
implicit val memberValidator = BaseValidator[Member].path(_.age)(min(18) && max(40))
val invalidMember = Member(Name("Ada"), 0)
val messages = invalidMember.validate.list
assert(messages == List(".age should be greater than 18"))
You can find full list of validators that provided out of the box in dupin.instances.DupinInstances
Message customization
But not many real projects use strings as validation messages, for example you want to support internationalization:
case class I18nMessage(
description: String,
key: String,
params: List[String]
)
As BaseValidator[R]
is just a type alias for Validator[String, R, Id]
, you can define own validator type with builder:
import dupin.all._
type I18nValidator[R] = Validator[I18nMessage, R, cats.Id]
def I18nValidator[R] = Validator[I18nMessage, R, cats.Id]
And start creating validators with custom messages:
import dupin.all._
implicit val nameValidator = I18nValidator[Name].root(_.value.nonEmpty, c => I18nMessage(
c.path + " should be non empty",
"validator.name.empty",
List(c.path.toString())
))
implicit val memberValidator = I18nValidator[Member]
.combinePI(_.name)
.combinePR(_.age)(a => a > 18 && a < 40, c => I18nMessage(
c.path + " should be between 18 and 40",
"validator.member.age",
List(c.path.toString())
))
Validation messages will look like:
import dupin.all._
val invalidMember = Member(Name(""), 0)
val messages: List[I18nMessage] = invalidMember.validate.list
assert(messages == List(
I18nMessage(
".name should be non empty",
"validator.name.empty",
List(".name")
),
I18nMessage(
".age should be between 18 and 40",
"validator.member.age",
List(".age")
)
))
Kind customization
For example you want to allow only using of limited list of names and they are stored in the database:
import scala.concurrent.Future
class NameService {
private val allowedNames = Set("Ada")
def contains(name: String): Future[Boolean] =
// Emulation of DB call
Future.successful(allowedNames(name))
}
So to be able to handle checks that returns Future[Boolean]
, you just need to define your own validator type with builder:
import cats.Applicative
import dupin.all._
import scala.concurrent.Future
type FutureValidator[R] = Validator[String, R, Future]
def FutureValidator[R](implicit A: Applicative[Future]) = Validator[String, R, Future]
Then you can create validators with generic dsl (don't forget to import required type classes, as minimum Functor[Future]
):
import cats.implicits._
import dupin.all._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
val nameService = new NameService
implicit val nameValidator = FutureValidator[Name].root(
n => nameService.contains(n.value), _.path + " should be non empty"
)
implicit val memberValidator = FutureValidator[Member]
.combinePI(_.name)
.combinePR(_.age)(a => Future.successful(a > 18 && a < 40), _.path + " should be between 18 and 40")
Validation result will look like:
import cats.data.NonEmptyList
import cats.implicits._
import dupin.all._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
val invalidMember = Member(Name(""), 0)
val messages: Future[Either[NonEmptyList[String], Member]] = invalidMember.validate.map(_.either)
assert(Await.result(messages, Duration.Inf) == Left(NonEmptyList.of(
".name should be non empty",
".age should be between 18 and 40"
)))
Custom validating package
To avoid imports boilerplate and isolating all customizations you can define your own dupin package:
package dupin
import cats.Applicative
import cats.instances.FutureInstances
import dupin.instances.DupinInstances
import dupin.syntax.DupinSyntax
import scala.concurrent.Future
package object custom
extends DupinCoreDsl with DupinInstances with DupinSyntax
with FutureInstances {
type CustomValidator[R] = Validator[I18nMessage, R, Future]
def CustomValidator[R](implicit A: Applicative[Future]) = Validator[I18nMessage, R, Future]
}
Then you can start using your own validator type with single import:
import dupin.custom._
import scala.concurrent.ExecutionContext.Implicits.global
val nameService = new NameService
implicit val nameValidator = CustomValidator[Name](
n => nameService.contains(n.value), c => I18nMessage(
c.path + " should be non empty",
"validator.name.empty",
List(c.path.toString())
)
)
val validName = Name("Ada")
assert(Await.result(validName.isValid, Duration.Inf))
Integration example
For example you are using play framework in your project, then instead of parsing dtos directly from JsValue
, you can create your own wrapper around it and inject validation logic there, like:
import cats.data.NonEmptyList
import dupin.all._
import dupin.core.Success
import play.api.libs.json.JsValue
import play.api.libs.json.Reads
case class JsonContent(jsValue: JsValue) {
def as[A: Reads](
implicit validator: BaseValidator[A] = BaseValidator.success
): Either[NonEmptyList[String], A] = {
jsValue.validate[A].asEither match {
case Right(value) => validator.validate(value).either
case Left(errors) => Left(NonEmptyList(errors.toString, Nil))
}
}
}
= BaseValidator.success
- will allow you to successfully parse dtos that don't have validator.
*Note that all licence references and agreements mentioned in the Dupin README section above
are relevant to that project's source code only.