Validation Framework Integration

Finatra provides an integration with the util-validator c.t.util.validation.ScalaValidator for use in validating the fields of a Scala case class.

For more information on the util-validator library, please see the documentation.

Using a customized ScalaValidator

You can customize and provide a ScalaValidator via a TwitterModule. The easiest way to do so is to extend the c.t.finatra.validation.ValidatorModule and implement the ValidatorModule#configureValidator method:

 import com.twitter.finatra.validation.ValidatorModule
 import com.twitter.inject.Injector
 import com.twitter.util.validation.ScalaValidator
 import com.twitter.util.validation.cfg.ConstraintMapping

 object CustomizedValidatorModule extends ValidatorModule {
   override def configureValidator(injector: Injector, builder: ScalaValidator.Builder): ScalaValidator.Builder = {
     val newConstraintMapping = ConstraintMapping(
        annotationType = classOf[FooConstraintAnnotation],
        constraintValidator = classOf[FooConstraintValidator]
      )

     builder
       .withDescriptorCacheSize(512)
       .withConstraintMapping(newConstraintMapping)
   }
 }

Then include this module in your server’s list of modules. In a Finatra HTTP server, a ScalaValidator is provided as a framework module, thus you can use your new module as the implementation of the validatorModule function in your HttpServer definition:

 import com.twitter.finatra.http.HttpServer
 import com.twitter.finatra.http.routing.HttpRouter
 import com.twitter.inject.TwitterModule

 ServerWithCustomizedValidator extends HttpServer {
   override val name: String = "validation-server"
   override def validatorModule: TwitterModule = CustomizedValidatorModule

   override protected def configureHttp(router: HttpRouter): Unit = {
     router
       .filter[MyFilter]
       .add[MyController]
       ...
   }
 }

Important

By overriding the default validatorModule in an HttpServer, you are also replacing the default ScalaValidator used by the framework ScalaObjectMapper provided by the default framework jacksonModule.

The newly defined ScalaValidator will be used to apply the validation logic in the ScalaObjectMapper.

See Integration with Finatra Jackson Support for more information about how case class validations work in the Finatra Jackson Framework.

Integration with Finatra Jackson Support

The Finatra framework integrates with Jackson in a couple of ways.

ScalaObjectMapper

The first and most straight-forward is through the ScalaObjectMapper. The ScalaObjectMapper directly integrates with the util-validator library via the case class deserializer to efficiently execute field and method validations as a part of JSON deserialization. For details on case class validation during JSON deserialization, please refer to JSON Validation Framework.

HTTP Routing

Secondly, Jackson support is also integrated with Finatra’s HTTP Routing to allow for validation of JSON request bodies when modeled as a custom request case class.

For example, if you have the following HTTP custom request case class defined:

import jakarta.validation.constraints.{Max, NotEmpty}
import com.twitter.util.validation.constraints.Pattern

case class ValidateUserRequest(
  @NotEmpty @Pattern(regexp = "[a-z]+") userName: String,
  @Max(value = 9999) id: Long,
  title: String
)

And in your HTTP controller, you define a POST endpoint:

post("/validate_user") { _: ValidateUserRequest =>
  ...
}

When a POST request with a content-type of application/json is sent to the /validate_user/ endpoint of the server, Finatra will automatically deserialize the incoming request body into a ValidationUserRequest case class. During deserialization the annotated constraints will be applied to the fields as part of instance construction.

If any validation constraint fails during JSON deserialization, a CaseClassMappingException will be thrown which is handled by the CaseClassExceptionMapper in a Finatra HttpServer by default to translate the exception into a suitable HTTP response.

Note

Non-recoverable validation errors are generally thrown as jakarta.validation.ValidationException which are handled by the ThrowableExceptionMapper <https://github.com/twitter/finatra/blob/develop/http-server/src/main/scala/com/twitter/finatra/http/internal/exceptions/ThrowableExceptionMapper.scala> in a Finatra HttpServer by default to translate the exception into a suitable HTTP response.

Best Practices, Guidance & Limitations

Please refer to the util-validator documentation for details.

  • Case classes used for validation should be side-effect free under property access. That is, accessing a case class field should be able to be done eagerly and without side-effects.

  • Case classes with generic type params should be considered not supported for validation. E.g.,

    case class GenericCaseClass[T](@NotEmpty @Valid data: T)
    

    This may appear to work for some category of data but in practice the way the util-validator caches reflection data does not discriminate on the type binding and thus validation of genericized case classes is not guaranteed to be successful for differing type bindings of a given genericized class. Note that this case works in the Jackson integration due to different caching semantics and how Jackson deserialization works where the binding type information is known.

  • While Iterable collections are supported for validation when annotated with @Valid, this does not include Map types. Annotated Map types will be ignored during validation.

  • More generally, types with multiple type params, e.g. Either[T, U], are not supported for validation of contents when annotated with @Valid. Annotated unsupported types will be ignored during validation.