Skip to content

Commit

Permalink
finatra-jackson: add ability to bypass case class validation
Browse files Browse the repository at this point in the history
Problem

The `finatra/jackson` project bundles multiple deserialization
features into `FinatraCaseClassDeserializer`. There is not a
way to bypass case class validation while still maintaining
feature compatability with the `FinatraObjectMapper`.

An example use case for this would be receiving a `POST` request
with a JSON body parsed by the `FinatraObjectMapper`, which does
some validation. This content is then stored somewhere. The
case class representations may change or introduce new validations
after that data has been stored. If that data is retrieved and
parsed via the `FinatraObjectMapper`, it will fail validation.

Solution

Introduce a `NullValidationFinatraJacksonModule` that can
be used to bypass case class validation, while retaining
the `FinatraObjectMapper`'s feature set.

Result

Users of the framework can now use the `NullValidationFinatraJacksonModule`
to optionally bypass case class validation.

Differential Revision: https://phabricator.twitter.biz/D307795
  • Loading branch information
enbnt authored and jenkins committed May 1, 2019
1 parent feb887e commit 401d728
Show file tree
Hide file tree
Showing 22 changed files with 203 additions and 32 deletions.
25 changes: 20 additions & 5 deletions CHANGELOG.rst
Expand Up @@ -7,22 +7,37 @@ Note that ``RB_ID=#`` and ``PHAB_ID=#`` correspond to associated message in comm
Unreleased
----------

Added
~~~~~

* finatra-jackson: Add ability to bypass case class validation using the
`NullValidationFinatraJacksonModule`. ``PHAB_ID=D307795``

* inject-app: Add `c.t.inject.app.DtabResolution` to help users apply supplemental Dtabs added by
setting the dtab.add flag. This will append the supplemental Dtabs to the
Dtab.base in a premain function. ``PHAB_ID=D303813``

Changed
~~~~~~~

* inject-app: Move override of `com.twitter.app.App#failfastOnFlagsNotParsed` up from
`c.t.inject.server.TwitterServer` to `com.twitter.inject.app.App` such that all Finatra-based
applications default to this behavior. ``PHAB_ID=D307858``

* inject-app|server: Fix capturing of flag ordering from Modules for adding to the App's `c.t.app.Flags`
* inject-app|server: change capturing of flag ordering from Modules for adding to the App's `c.t.app.Flags`
instance to match the semantics of directly calling `c.t.app.Flags#add`. Prefer `AtomicBoolean`
instances over where we currently use mutable `Boolean` instances in `c.t.inject.app.App`, `c.t.inject.app.TestInjector`,
and `c.t.inject.server.EmbeddedTwitterServer`. ``PHAB_ID=D306897``

* inject-app: Add `c.t.inject.app.DtabResolution` to help users apply supplemental Dtabs added by
setting the dtab.add flag. This will append the supplemental Dtabs to the
Dtab.base in a premain function. ``PHAB_ID=D303813``

* finatra-examples: Update "twitter-clone" example to use `Dtabs` instead of the deprecated `resolverMap`.
Move the "hello-world" example to "http-server". ``PHAB_ID=D303813``

Fixed
~~~~~

Closed
~~~~~~

19.4.0
------

Expand Down
18 changes: 18 additions & 0 deletions doc/src/sphinx/user-guide/json/validations.rst
Expand Up @@ -49,3 +49,21 @@ Eg.,
"nsfw: 'abc' is not a valid boolean"
]
}
Bypassing Validation
--------------------

You may desire to execute validation for specific case classes in certain scenarios, but bypass validation in others.
For example, you may want to validate a `POST` request on the write path and store the JSON results somewhere, but
bypass validating that same JSON for a `GET` request on the read path.

You can create a `FinatraObjectMapper <https://github.com/twitter/finatra/blob/develop/jackson/src/main/scala/com/twitter/finatra/json/FinatraObjectMapper.scala>`__
that will bypass validation like this:

.. code:: scala
def create(injector: Injector = null): FinatraObjectMapper = {
val jacksonModule = NullValidationFinatraJacksonModule
new FinatraObjectMapper(
jacksonModule.provideScalaObjectMapper(injector))
}
Expand Up @@ -6,7 +6,7 @@ import java.lang.annotation.Annotation
/**
* Trait for defining a validator that will be triggered during Case Class validation.
*/
private[finatra] trait CaseClassValidator {
private[json] trait CaseClassValidator {

def validateField[V](
fieldValue: V,
Expand Down
Expand Up @@ -2,7 +2,7 @@ package com.twitter.finatra.json.internal.caseclass.validation

import com.twitter.finatra.validation.ValidationMessageResolver

private[finatra] object DefaultValidationProvider extends ValidationProvider {
private[json] object DefaultValidationProvider extends ValidationProvider {

override def apply(): CaseClassValidator = {
val messageResolver = new ValidationMessageResolver
Expand Down
@@ -0,0 +1,18 @@
package com.twitter.finatra.json.internal.caseclass.validation

import com.twitter.finatra.validation.ValidationResult
import java.lang.annotation.Annotation

/**
* No-op validator, which will treat any field values as acceptable during
* Finatra Validation of annotations.
*/
private[json] object NullCaseClassValidator extends CaseClassValidator {

override def validateField[V](
fieldValue: V,
fieldValidationAnnotations: Seq[Annotation]
): Seq[ValidationResult] = Seq.empty

override def validateObject(obj: Any): Seq[ValidationResult] = Seq.empty
}
@@ -0,0 +1,8 @@
package com.twitter.finatra.json.internal.caseclass.validation

/**
* Provides a validation bypass for case class validation.
*/
private[json] object NullValidationProvider extends ValidationProvider {
override def apply(): CaseClassValidator = NullCaseClassValidator
}
Expand Up @@ -3,7 +3,7 @@ package com.twitter.finatra.json.internal.caseclass.validation
/**
* Trait that defines a factory for returning a CaseClassValidator.
*/
private[finatra] trait ValidationProvider {
private[json] trait ValidationProvider {

/**
* Return a CaseClassValidator instance that will be used to provide validation against
Expand Down
Expand Up @@ -10,7 +10,7 @@ import com.twitter.finatra.validation.{
}
import java.util.Locale

private[finatra] object CountryCodeValidator {
private[json] object CountryCodeValidator {

def errorMessage(resolver: ValidationMessageResolver, value: Any) = {

Expand All @@ -29,7 +29,7 @@ private[finatra] object CountryCodeValidator {
}
}

private[finatra] class CountryCodeValidator(
private[json] class CountryCodeValidator(
validationMessageResolver: ValidationMessageResolver,
annotation: CountryCode
) extends Validator[CountryCode, Any](validationMessageResolver, annotation) {
Expand Down
Expand Up @@ -10,7 +10,7 @@ import com.twitter.finatra.validation.{
}
import org.joda.time.DateTime

private[finatra] object FutureTimeValidator {
private[json] object FutureTimeValidator {

def errorMessage(resolver: ValidationMessageResolver, value: DateTime) = {

Expand All @@ -21,7 +21,7 @@ private[finatra] object FutureTimeValidator {
/**
* Validates if a datetime is in the future.
*/
private[finatra] class FutureTimeValidator(
private[json] class FutureTimeValidator(
validationMessageResolver: ValidationMessageResolver,
annotation: FutureTime
) extends Validator[FutureTime, DateTime](validationMessageResolver, annotation) {
Expand Down
Expand Up @@ -8,15 +8,15 @@ import com.twitter.finatra.validation.{
Validator
}

private[finatra] object MaxValidator {
private[json] object MaxValidator {

def errorMessage(resolver: ValidationMessageResolver, value: Any, maxValue: Long): String = {

resolver.resolve(classOf[Max], value, maxValue)
}
}

private[finatra] class MaxValidator(
private[json] class MaxValidator(
validationMessageResolver: ValidationMessageResolver,
annotation: Max
) extends Validator[Max, Any](validationMessageResolver, annotation) {
Expand Down
Expand Up @@ -8,15 +8,15 @@ import com.twitter.finatra.validation.{
Validator
}

private[finatra] object MinValidator {
private[json] object MinValidator {

def errorMessage(resolver: ValidationMessageResolver, value: Any, minValue: Long): String = {

resolver.resolve(classOf[Min], value, minValue)
}
}

private[finatra] class MinValidator(
private[json] class MinValidator(
validationMessageResolver: ValidationMessageResolver,
annotation: Min
) extends Validator[Min, Any](validationMessageResolver, annotation) {
Expand Down
Expand Up @@ -9,15 +9,15 @@ import com.twitter.finatra.validation.{
Validator
}

private[finatra] object NotEmptyValidator {
private[json] object NotEmptyValidator {

def errorMessage(resolver: ValidationMessageResolver) = {

resolver.resolve(classOf[NotEmpty])
}
}

private[finatra] class NotEmptyValidator(
private[json] class NotEmptyValidator(
validationMessageResolver: ValidationMessageResolver,
annotation: NotEmpty
) extends Validator[NotEmpty, Any](validationMessageResolver, annotation) {
Expand Down
Expand Up @@ -9,7 +9,7 @@ import com.twitter.finatra.validation.{
Validator
}

private[finatra] object OneOfValidator {
private[json] object OneOfValidator {

def errorMessage(resolver: ValidationMessageResolver, oneOfValues: Set[String], value: Any) = {

Expand All @@ -36,7 +36,7 @@ private[finatra] object OneOfValidator {
* Validates if one or more values exist in a given set of values. The check for existence is case-sensitive
* by default.
*/
private[finatra] class OneOfValidator(
private[json] class OneOfValidator(
validationMessageResolver: ValidationMessageResolver,
annotation: OneOf
) extends Validator[OneOf, Any](validationMessageResolver, annotation) {
Expand Down
Expand Up @@ -10,7 +10,7 @@ import com.twitter.finatra.validation.{
}
import org.joda.time.DateTime

private[finatra] object PastTimeValidator {
private[json] object PastTimeValidator {

def errorMessage(resolver: ValidationMessageResolver, value: DateTime) = {

Expand All @@ -21,7 +21,7 @@ private[finatra] object PastTimeValidator {
/**
* Validates if a datetime is in the past.
*/
private[finatra] class PastTimeValidator(
private[json] class PastTimeValidator(
validationMessageResolver: ValidationMessageResolver,
annotation: PastTime
) extends Validator[PastTime, DateTime](validationMessageResolver, annotation) {
Expand Down
Expand Up @@ -6,7 +6,7 @@ import com.twitter.finatra.validation._
import com.twitter.util.{Return, Throw, Try}
import scala.util.matching.Regex

private[finatra] object PatternValidator {
private[json] object PatternValidator {
def errorMessage(resolver: ValidationMessageResolver, value: Any, regex: String): String = {
resolver.resolve(classOf[Pattern], value, regex)
}
Expand All @@ -19,7 +19,7 @@ private[finatra] object PatternValidator {
* case class ExampleRequest(@Pattern(regexp= "exampleRegex") exampleValue : String)
* }}}
*/
private[finatra] class PatternValidator(
private[json] class PatternValidator(
validationMessageResolver: ValidationMessageResolver,
annotation: Pattern)
extends Validator[Pattern, Any](validationMessageResolver, annotation) {
Expand Down
Expand Up @@ -8,7 +8,7 @@ import com.twitter.finatra.validation.{
Validator
}

private[finatra] object RangeValidator {
private[json] object RangeValidator {

def errorMessage(
resolver: ValidationMessageResolver,
Expand All @@ -21,7 +21,7 @@ private[finatra] object RangeValidator {
}
}

private[finatra] class RangeValidator(
private[json] class RangeValidator(
validationMessageResolver: ValidationMessageResolver,
annotation: Range
) extends Validator[Range, Any](validationMessageResolver, annotation) {
Expand Down
Expand Up @@ -9,7 +9,7 @@ import com.twitter.finatra.validation.{
Validator
}

private[finatra] object SizeValidator {
private[json] object SizeValidator {

def errorMessage(
resolver: ValidationMessageResolver,
Expand All @@ -35,7 +35,7 @@ private[finatra] object SizeValidator {
}
}

private[finatra] class SizeValidator(
private[json] class SizeValidator(
validationMessageResolver: ValidationMessageResolver,
annotation: Size
) extends Validator[Size, Any](validationMessageResolver, annotation) {
Expand Down
Expand Up @@ -12,7 +12,7 @@ import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit._
import org.joda.time.{DateTime, DateTimeZone}

private[finatra] object TimeGranularityValidator {
private[json] object TimeGranularityValidator {

def errorMessage(
resolver: ValidationMessageResolver,
Expand All @@ -32,7 +32,7 @@ private[finatra] object TimeGranularityValidator {
/**
* Validates if a given value is of a given time granularity (e.g., days, hours, seconds)
*/
private[finatra] class TimeGranularityValidator(
private[json] class TimeGranularityValidator(
validationMessageResolver: ValidationMessageResolver,
annotation: TimeGranularity
) extends Validator[TimeGranularity, DateTime](validationMessageResolver, annotation) {
Expand Down
Expand Up @@ -11,7 +11,7 @@ import com.twitter.finatra.validation.{
import com.twitter.util.Try
import java.util.{UUID => JUUID}

private[finatra] object UUIDValidator {
private[json] object UUIDValidator {

def errorMessage(resolver: ValidationMessageResolver, value: String) = {

Expand All @@ -23,7 +23,7 @@ private[finatra] object UUIDValidator {
}
}

private[finatra] class UUIDValidator(
private[json] class UUIDValidator(
validationMessageResolver: ValidationMessageResolver,
annotation: UUID
) extends Validator[UUID, String](validationMessageResolver, annotation) {
Expand Down
@@ -0,0 +1,14 @@
package com.twitter.finatra.json.modules

import com.fasterxml.jackson.module.scala._
import com.twitter.finatra.json.internal.caseclass.jackson.CaseClassDeserializers
import com.twitter.finatra.json.internal.caseclass.validation.NullValidationProvider

/**
* Module that supports skipping validation of Finatra validation annotations.
*/
private[json] object NullValidationCaseClassModule extends JacksonModule {
override def getModuleName = "NullValidationCaseClassModule"

this += { _.addDeserializers(new CaseClassDeserializers(NullValidationProvider)) }
}
@@ -0,0 +1,13 @@
package com.twitter.finatra.json.modules

/**
* Provides a FinatraJacksonModule that will treat all fields as acceptable during
* Finatra Validation of annotations.
*/
object NullValidationFinatraJacksonModule extends NullValidationFinatraJacksonModule

class NullValidationFinatraJacksonModule extends FinatraJacksonModule {

override val finatraCaseClassModule = Some(NullValidationCaseClassModule)

}

0 comments on commit 401d728

Please sign in to comment.