JSON请求验证器


软件开发人员可以永远关注三样东西:火、水和请求验证器。在previous blog在文章中,我演示了如何用http4s开发一个简单的REST应用程序接口。但是该应用程序有一个缺点——用户可以向应用程序接口发送任何无效的数据,并且在处理时不会出现错误信息。

你可能还喜欢:Implementing Validation for RESTful Service With Spring Boot

问题

99.99%的网络应用的主要问题是他们有用户(并且可以自由发推)。因此,有一个巨大的人为因素,这导致了与web应用程序的大量不适当的交互。我最喜欢的一个是无效的数据输入。例如,用户可以在电子邮件字段中输入“blablabla”,在电话号码字段中输入“my昵称”,甚至在需要时将表单字段留空。

事实上,任何应用程序中的每个数据模型都有自己的验证规则。让我们考虑一个图书模型:

type Title = String
type Author = String

case class Book(title: Title, author: Author)


为简单起见,让我们假设titleauthor只有一个要求——不是空字段。从应用编程接口的角度来看,这意味着什么?如果这些字段中的任何一个为空,则响应应为400 Bad Request;此外,它应该包含验证错误的解释:

[
    {
        "fieldName": "title",
        "message": "Must not be empty"
    },
    {
        "fieldName": "author",
        "message": "Must not be empty"
    }
]


有了这样的回应,很容易反思用户界面到底出了什么问题。

http4s中的JSON验证

我花了大量时间研究如何更好地组织JSON请求验证。在长时间的讨论和StackOverFlow线程中,我想出了以下解决方案。首先,我们需要声明一些描述可能的验证错误的数据模型:

import cats.data.NonEmptyList

type FieldName = String
type Message = String

final case class FieldError(fieldName: FieldName, message: Message)

type ValidationResult = Option[NonEmptyList[FieldError]]


现在,我们可以继续使用这些模型来定义两个验证抽象:

trait Validator[T] {
  def validate(target: T): ValidationResult
}

trait FieldValidator[T] {
  def validate(field: T, fieldName: FieldName): ValidationResult
}


这里,Validator[T]用于描述特定案例类的某个验证器。然而FieldValidator[T]旨在为案例类的特定字段描述特定的验证规则。现在,我们可以进行下一个逻辑步骤,创建一些真正的字段验证器。让我们假设我们想要确保String类型字段不能为空:

case object NotEmpty extends FieldValidator[String] {
  def validate(target: String, fieldName: FieldName) =
    if (target.isEmpty) NonEmptyList.of(FieldError(fieldName, "Must not be empty")).some else None
}


正如你所看到的NotEmptycase类描述了这个规则。一个更灵活的例子怎么样?假设我们需要验证一个String按长度键入字段:

case class WithLength(min: Int, max: Int) extends FieldValidator[String] {
  def validate(target: String, fieldName: FieldName) =
    if (target.length < min || target.length > max)
      NonEmptyList.of(FieldError(fieldName, s"Length must be between $min and $max")).some else None
}


通过使用相同的方法,您可以为任意类型创建任何自定义规则。

现在,是时候回到书籍休息应用编程接口,并确保我们不允许处理一个Book具有空字段的模型。

object Book {
  import cats.implicits._
  import FieldValidator.strings._
  implicit val validator: Validator[Book] = (target: Book) => {
    NotEmpty.validate(target.title, "title") |+|
    NotEmpty.validate(target.author, "author")
  }
}


从代码片段中,您可以看到我添加了Validator[Book]Book伴侣对象。因此,验证规则总是随模型本身一起提供。出于演示的目的,我想展示一下添加是多么容易Bookhttp4s路由的模型验证:

...
case req @ POST -> Root / "books" =>
  req.decode[Book] { book =>
    Book.validator.validate(book) match {
      case None =>
        bookRepo.addBook(book).flatMap(id =>
          Created(Json.obj(("id", Json.fromString(id.value))))
        )
      case Some(errors) => BadRequest(errors)
    }
  }
...


瞧。你的晚餐准备好了!

从代码维护的角度来看,这种方法非常方便,而且通过引入可重用性,它可以很好地扩展FieldValidator[T]实现。

摘要

在本文中,我描述了如何为数据验证建立一致的方法。它的灵感来自与Scala社区的对话,可能有一些缺点,出于某种原因我没有注意到。所以我很乐意阅读任何反馈。

进一步阅读

Implementing Validation for RESTful Service With Spring Boot

How to Use Validate JSON Schema and Message Enricher in Mule

[DZone Refcard] Core JSON