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

Open
cerker opened this issue Jun 14, 2021 · 5 comments
Open

Cross-field validation #29

cerker opened this issue Jun 14, 2021 · 5 comments
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.

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

No branches or pull requests

7 participants