Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cross-field validation #29

Closed
cerker opened this issue Jun 14, 2021 · 7 comments · Fixed by #171
Closed

Cross-field validation #29

cerker opened this issue Jun 14, 2021 · 7 comments · Fixed by #171
Labels
enhancement New feature or request

Comments

@cerker
Copy link

cerker commented Jun 14, 2021

Is it possible to validate one field in relation to others?

For example, my user has a validFrom: LocalDateTime and a validUntil: LocalDateTime? field. How can I validate that validUntil is after validFrom?

I started writing a custom validator:

fun ValidationBuilder<LocalDateTime>.isAfter(other: LocalDateTime): Constraint<LocalDateTime> {
    return addConstraint(
        "must be after {0}",
        other.toString()
    ) { it.isAfter(other) }
}

but how can I pass the other value?

UserProfile::validUntil ifPresent {
        isAfter(???)
}
@aSemy
Copy link

aSemy commented Jul 17, 2021

What do you think about using run to conditionally include specific validators?

EDIT: this doesn't work as I expected - ifPresent doesn't run conditionally, and will always execute the run , which adds the validation whether validUntil is null or not.

import java.time.LocalDate
import java.time.LocalDateTime
import io.konform.validation.Validation

data class User(
    val validFrom: LocalDateTime,
    val validUntil: LocalDateTime?,
) {
  companion object {
    /** validate two fields on User */
    private val validateUserDates = Validation<User> {
      addConstraint("before/after") {
        it.validUntil?.isAfter(it.validFrom) ?: false
      }
    }

    /** public validator, combining private validators */
    val validateUser = Validation<User> {
      User::validUntil ifPresent {
        run(validateUserDates)
      }
    }

  }
}

fun main() {
  val time = LocalDate.of(2020, 1, 1).atStartOfDay()

  val userValid = User(time, time.plusDays(10))
  println(User.validateUser(userValid))
  // Valid(value=User(validFrom=2020-01-01T00:00, validUntil=2020-01-11T00:00))

  val userInvalid = User(time, time.plusDays(-10))
  println(User.validateUser(userInvalid))
  // Invalid(errors=[ValidationError(dataPath=, message=before/after)])

  // EDIT: the `ifPresent` didn't work as I expected
  val userNoUntil = User(time, null)
  println(User.validateUser(userNoUntil))
  // Invalid(errors=[ValidationError(dataPath=, message=before/after)])
}

The downside is that the error message isn't dynamic. It would be nice to have a Constraint implementation that can dynamically create a message.

@JohannesZick
Copy link

I'm a little surprised this feature is missing. I keep running into this, and it feels like the most missed feature in good ol' JSR303.

Example I have right now: validate a postal code depending on the country.

Basically, it should be possible to select validations to apply to an object dynamically from the objects runtime state, not just statically. Assuming custom validations:

val validateAddress = Validation<Address> {
    Address::countryCode {
        validIsoCountryCode()
    }

    Address::postalCode dynamicValidation { it: Address ->
        when (it.countryCode) {
            "US" -> validUsPostalCode()
            "DE" -> validGermanPostalCode()
            else -> // default validation of some kind
        }
    }
}

@nlochschmidt nlochschmidt added the enhancement New feature or request label May 3, 2022
@floatdrop
Copy link

I'm not quite sure, why ValidationBuilder should not give reference inside init block to the variable under validation:

val validateAddress = Validation<Address> { it: Address ->
    Address::countryCode {
        validIsoCountryCode()
    }

    Address::postalCode {
        when (it.countryCode) {
            "US" -> validUsPostalCode()
            "DE" -> validGermanPostalCode()
            else -> // default validation of some kind
        }
    }
}

Is there a reason behind building static validators (caching validators by PropKey?)?

@minisaw
Copy link

minisaw commented Sep 30, 2022

Is it possible to validate one field in relation to others?

For example, my user has a validFrom: LocalDateTime and a validUntil: LocalDateTime? field. How can I validate that validUntil is after validFrom?

doesn't the following snippet address your scenario?

data class User(val validFrom: LocalDateTime, val validUntil: LocalDateTime?) {
    companion object {
        val validateUser = Validation<User> {
            addConstraint("validUntil, if present, must be after validFrom") {
                it.validUntil?.isAfter(it.validFrom) ?: true
            }
        }
    }
}

@lnhrdt
Copy link

lnhrdt commented Apr 18, 2024

doesn't the following snippet address your scenario?

Hey @minisaw I'm not @cerker but the limitation of what you suggested is that the error is associated with the top level value, not the field (i.e. the dataPath will be "" rather than ".validUntil").

@nlochschmidt do you have any intention to support cross-field validation in konform? I tried looking through the issues for more context on this feature request.

@dhoepelman
Copy link
Collaborator

0.7.0 with #86 will improve the situation by making it possible, but it's not quite where I want it yet.
Here's how it would work for the address example

val validateAddress = Validation<Address> {

    validate("postalCode", { it.countryCode to it.postalCode }) {
      val postCodeValidationForCountry = when(it.first) {
         "US" -> validateusPostalCode
         "DE" -> validGermanPostalCode()
         else -> // default validation of some kind
      } 
      validate(countryCode, { it.second }) {
        run(postCodeValidationForCountry)
      }
   }
}

@dhoepelman
Copy link
Collaborator

I believe #171 adequately addresses this. See the new README and tests for examples. Here's how this version solves #29 (comment)

Validation<Address> {
    Address::postalCode dynamic { address ->
        when (address.countryCode) {
            "US" -> pattern("[0-9]{5}")
            else -> pattern("[A-Z]+")
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants