GithubHelp home page GithubHelp logo

konform-kt / konform Goto Github PK

View Code? Open in Web Editor NEW
624.0 8.0 38.0 428 KB

Portable validations for Kotlin

Home Page: https://www.konform.io

License: MIT License

Kotlin 100.00%
kotlin kotlin-multiplatform validation

konform's Introduction

Test Maven Central

Portable validations for Kotlin

  • โœ… Type-safe DSL
  • ๐Ÿ”— Multi-platform support (JVM, JS, Native, Wasm)
  • ๐Ÿฅ Zero dependencies

Installation

For multiplatform projects:

kotlin {
    sourceSets {
        commonMain {
            dependencies {
                implementation("io.konform:konform:0.6.0")
            }
        }
    }
}

For jvm-only projects add:

dependencies {
    implementation("io.konform:konform-jvm:0.6.0")
}

Use

Suppose you have a data class like this:

data class UserProfile(
    val fullName: String,
    val age: Int?
)

Using the Konform type-safe DSL you can quickly write up a validation

val validateUser = Validation<UserProfile> {
    UserProfile::fullName {
        minLength(2)
        maxLength(100)
    }

    UserProfile::age ifPresent {
        minimum(0)
        maximum(150)
    }
}

and apply it to your data

val invalidUser = UserProfile("A", -1)
val validationResult = validateUser(invalidUser)

since the validation fails the validationResult will be of type Invalid and you can get a list of validation errors by indexed access:

validationResult[UserProfile::fullName]
// yields listOf("must have at least 2 characters")

validationResult[UserProfile::age]
// yields listOf("must be at least '0'")

or you can get all validation errors with details as a list:

validationResult.errors
// yields listOf(
//     ValidationError(dataPath=.fullName, message=must have at least 2 characters),
//     ValidationError(dataPath=.age, message=must be at least '0'
// )

In case the validation went through successfully you get a result of type Valid with the validated value in the value field.

val validUser = UserProfile("Alice", 25)
val validationResult = validateUser(validUser)
// yields Valid(UserProfile("Alice", 25))

Advanced use

Hints

You can add custom hints to validations

val validateUser = Validation<UserProfile> {
    UserProfile::age ifPresent {
        minimum(0) hint "Registering before birth is not supported"
    }
}

You can use {value} to include the .toString()-ed data in the hint

val validateUser = Validation<UserProfile> {
    UserProfile::fullName {
        minLength(2) hint "'{value}' is too short a name, must be at least 2 characters long."
    }
}

Custom validations

You can add custom validations by using addConstraint

val validateUser = Validation<UserProfile> {
    UserProfile::fullName {
        addConstraint("Name cannot contain a tab") { !it.contains("\t") }
    }
}

Nested validations

You can define validations for nested classes and use them for new validations

val ageCheck = Validation<UserProfile> {
    UserProfile::age required {
        minimum(18)
    }
}

val validateUser = Validation<UserProfile> {
    UserProfile::fullName {
        minLength(2)
        maxLength(100)
    }

    run(ageCheck)
}

It is also possible to validate nested data classes and properties that are collections (List, Map, etc...)

data class Person(val name: String, val email: String?, val age: Int)

data class Event(
    val organizer: Person,
    val attendees: List<Person>,
    val ticketPrices: Map<String, Double?>
)

val validateEvent = Validation<Event> {
    Event::organizer {
        // even though the email is nullable you can force it to be set in the validation
        Person::email required {
            pattern("[email protected]") hint "Organizers must have a BigCorp email address"
        }
    }

    // validation on the attendees list
    Event::attendees {
        maxItems(100)
    }

    // validation on individual attendees
    Event::attendees onEach {
        Person::name {
            minLength(2)
        }
        Person::age {
            minimum(18) hint "Attendees must be 18 years or older"
        }
        // Email is optional but if it is set it must be valid
        Person::email ifPresent {
            pattern(".+@.+\..+") hint "Please provide a valid email address (optional)"
        }
    }

    // validation on the ticketPrices Map as a whole
    Event::ticketPrices {
        minItems(1) hint "Provide at least one ticket price"
    }

    // validations for the individual entries
    Event::ticketPrices onEach {
        // Tickets may be free in which case they are null
        Entry<String, Double?>::value ifPresent {
            minimum(0.01)
        }
    }
}

Errors in the ValidationResult can also be accessed using the index access method. In case of Iterables and Arrays you use the numerical index and in case of Maps you use the key as string.

// get the error messages for the first attendees age if any
result[Event::attendees, 0, Person::age]

// get the error messages for the free ticket if any
result[Event::ticketPrices, "free"]

Other validation libraries written in Kotlin

Integration with testing libraries

  • Kotest provides various matchers for use with Konform. They can be used in your tests to assert that a given object is validated successfully or fails validation with specific error messages. See documentation.
Maintainer

David Hoepelman (Current maintainer) Niklas Lochschmidt (Original author, co-maintainer)

License

MIT License

konform's People

Contributors

andrewliles avatar dhoepelman avatar jillesvangurp avatar nlochschmidt avatar renovate-bot avatar renovate[bot] avatar sksamuel avatar vitorfec avatar wtomi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

konform's Issues

Add support for other regex matching functions

Konform uses .match() internally for the pattern validation.

It would be nice if there was an option to choose which method would be used in the regex validation, for example being able to choose containsMatchIn() or find() instead ofย match().
This would make some specific usages of patternย a bit more efficient and easier to work with.

hasSize not working as expected

My expectation for hasSize(100) is that the property is exactly 100 characters long but the function is defined like this:
hasSize(min: Int = Int.MIN_VALUE, max: Int = Int.MAX_VALUE)
which translates to hasSize(100, Int.MAX_VALUE).
I think there should be a one parameter function hasSize(size: Int) to prevent these misunderstandings and the two parameter function should not have default values for min/max.

Whitelist of properties for validation

This feature can be handy when you have two objects (state before modification and after). After retrieving paths for properties that changed we could check only fields that changed with same validation as for whole object. This is also useful when object in DB are written with old version of validation, but should be modifiable with new version of validator.

Create separate extension functions for minItems and maxItems

For now minItems and maxItems defined on T type and checks T inside. It is more concise to define them like this:

@JvmName("minItemsIterable")
fun <T, E> ValidationBuilder<out Iterable<T>, E>.minItems(
    minSize: Int
) = addConstraint(
    "must have at least {0} items",
    minSize.toString()
) { it.count() >= minSize }

@JvmName("maxItemsIterable")
fun <T, E> ValidationBuilder<out Iterable<T>, E>.maxItems(
    maxSize: Int,
) = addConstraint(
    "must have at most {0} items",
    maxSize.toString()
) { it.count() <= maxSize }

Introspection to create input field validation in HTML DSL

I suspect it is not possible, but I have to check: I want to use Konform with HTML DSL and generate HTML form validation information (required, minlength etc). But to do that I would need some kind of introspection of which validations is applied to an Validation/Object.

Is it possible? It would make it really useful in combination with other things. ๐Ÿ˜ƒ

Found this so suspect it's not possible, but honestly not that intimate with deep Kotlin details: #27

Make validations context aware

Currently when you want to do a database call or check a value against some set of data, your only option is to wrap the "context" in the builder, e.g.

val allowedValues: Set<String> = calculateAllowedValues()
val validation = Validation<String> {
    addConstraint("This value is not allowed!") { allowedValues.contains(it) }
}
validation("wim")

The result is that a validation is tightly coupled to its "context". This is troublesome especially when the "context" is not constant.

Prettier would be:

val validation = Validation<Set<String>, String> {
    addConstraint("This value is not allowed!") { value -> this.contains(value) }
}
val allowedValues: Set<String> = calculateAllowedValues()
validation(allowedValues, "wim")

The changes in PR #61 allow the user to use a piece of context, with almost no breaking changes for those that don't need any context (the special case, with Unit context). The only breaking change is when you define your own extension function on a ValidationBuilder.

fun ValidationBuilder<String>.myOwnConstraintBuilder() = ...
// becomes:
fun ValidationBuilder<Unit, String>.myOwnConstraintBuilder() = ...

It makes it also possible to combine validations with different contexts (see for example test method composeValidationsWithContext):

val addressValidation = Validation<AddressContext, Address> {
    Address::country {
        addConstraint("Country is not allowed") {
            this.validCountries.contains(it)
        }
    }
}

val validation = Validation<Context, Register> {
    Register::home ifPresent {
        run(addressValidation, Context::subContext)
    }
}

Please consider merging this branch.

Remove ifPresent

UserProfile::age ifPresent {
    minimum(0)
    maximum(150)
}

Why it is neccesary to have ifPresent?
Just apply validation rules ony if value is not null in any case.

How to add a custom property to validations?

Maybe this is out of the scope of the library (apologies if it is), but... is there a way to add a custom property to validations?

For instance, I would like to add a severity property, but in order to do that it would have to be part of both Validation and ValidationResult (so that I can see the severity of the validations that have failed), and I cannot seem to find a way to do that.

Thanks in advance, and keep up the great work!

Add support for Swagger/OpenAPI

https://swagger.io/specification/#schema-object

Swagger/OpenAPI schema contains class/object definition and validation rules.
It will be great if this library will provide this information in order to be able to integrate it for full schema generation from code.

For example Spring or Micronaut can generate endpoints schema and integrate class definitions from this library.

Additional maintainers?..

Hi! We are considering using Konform in our company, but are slightly concerned with decreased velocity of the project lately. Would you be open to accepting new maintainers to ensure the long-term continuity of the project?

[Feature] Add the ability to activate validations for Properties depending of the state of another property

Example:

if a firstName was passed to a Person and this firstName is present then the user should also enter a familyName to pass the validation
if the firstName was not passed (null) then the validation for the familyName should not be active
Something like this should be possible:

data class Person(val name: String?, val familyName: String?)


 val validator = Validation<Person> {
        Person::name ifPresent {
            minLength(1)

            Person::familyName {
                minLength(1)
            }
        }

        Person::familyName ifPresent {
            minLength(1)

            Person::name {
                minLength(1)
            }

        }
    }

`onEach` working for nullable iterables

Currently, one cannot apply onEach validation if iterable field is nullable, for example val items: List<MyClass>?
There should be some way to do it either by making it possible to combine ifPresent with onEach or by making onEach treat a null iterable in the same way as an empty iterable. There could also be new method like onEachIfPresent

Sets the dataPath for the parent

data class Baby(
    val age: Int
)

The current error message is '. age must be less than 5 '.
I add Baby before .age.
e.g: 'Baby. age must be less than 5'

Allow access to subvalidations

It should be possible to access subvalidations of a complete validation for example to use it on input fields.

Probably something along the lines of

val validation = Validation<DTO> { DTO::firstName.has.minLength(2) }
val firstNameValidation: Validation<String> = validation[DTO::firstName]

Exposing Validation Metadata

Hey, I am working on an OpenAPI spec generator for Ktor https://github.com/bkbnio/kompendium and would love to build in Konform validation, but I am running into a problem. A method for generating validation metadata is eluding me.

Because all implementations of Validation are internal, and afaik the actual constraints don't get exposed to the end user, it feels like I am in a bit of a bind as far as pulling that info goes.

So I have a couple questions

  1. Am I just missing something? Is there an easy way to pull constraint metadata for a declared validation
  2. If not, are there any major concerns with exposing the list of constraints applied to a validation?

Feature request: ability to validate string as a number.

Given: an HTTP request with a string parameter representing a number.
(btw, this is all HTTP requests as all parameters are strings, or arrays of strings!)

Then: I'd like to be able to validate that this string represents a number, and validate that number.

For example, given a request which posts a user's age, eg: "56".
I'd like to validate that the string is a number, and that it's a positive integer.

I imagine that this could be provided as ifNumber, in the same way as isPresent.

eg:

UserProfile::age ifNumber {
	minimum(0)
}

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

This repository currently has no open or pending branches.

Detected dependencies

github-actions
.github/workflows/release.yml
  • actions/checkout v4
  • actions/setup-java v4
  • trstringer/manual-approval v1
.github/workflows/test.yml
  • actions/checkout v4
  • actions/setup-java v4
gradle
gradle.properties
settings.gradle.kts
build.gradle.kts
  • org.jetbrains.kotlin.multiplatform 2.0.0
  • io.github.gradle-nexus.publish-plugin 2.0.0
  • org.jlleitschuh.gradle.ktlint 12.1.1
gradle-wrapper
gradle/wrapper/gradle-wrapper.properties
  • gradle 8.8

  • Check this box to trigger a request for Renovate to run again on this repository

Recursive validation support

Did not find anything related in the issues tab, so creating one to understand whats possible.

I have a set of data classes where one class references itself as a List. I'm unable to find a way to write a validator that can support this. Please share your thoughts on what can be done here.

Sample data classes -

data class Section(val components: List<Component>) {
    data class Component(val subComponents: List<Component>)
}

How to validate single values?

For example, how to create a validation for a String or LocalDate? without having them part of some other object

I was able to achieve by dropping to addConstraint:

va validation = Validation<String?> {
    addConstraint("must be 4 digits") { it != null && it.length == 4 }
}

but wasn't unable to utilize required, ifPresent or any of the existing validators (minLength, ...etc).

Is that supported?

Programatically list all errors

Currently, there is no way to programatically list all the errors associated with an object.

Yes, you can call ValidationResult.errors but the dataPath is computed in a non-public method, and there's no way to map it back to the property that errored without duplicated the computation.

Essentially I would like to do the following:

  1. define a check for each property
  2. map that check to a wire representation
  3. have control over the wire representation.

The Validation.dataPath method is a serialized representaiton of the error. I need a kotlin one.

I can almost get there simply by passing around references to the object properties (which has its own problems), but the handling for arrays puts it over the edge of inconvenience.

Question: Smart constructor pattern / composable validations

Hello, and thank you for this nice library. Coming from the FP world, I am trying to use it with the smart constructor pattern and, not having a bind (or flatMap, or andThen, or anything you call (ValidationResult<T1> -> (T1 -> ValidationResult<T2>)) -> ValidationResult<T2>), am having a bit of trouble: I'd be happy to open a PR with a bind implementation, but first I'm thinking maybe I'm using this wrong.

Using Konform, I've managed to build the following inline class, who'se type represents a proof of being valid (I don't want to have Valid<x>'s all over my domain code), which works well enough:

@JvmInline
value class CustomerID private constructor(val value: String) {
    companion object {
        operator fun invoke(input: String): ValidationResult<CustomerID> = Validation {
            pattern("^[A-Z0-9]{32}$") hint "Customer ID <value> does not match `^[A-Z0-9]{32}$`"
        }(CustomerID(input))
    }
}

But then when I have a class that is using the above value class as a member, I cannot compose both validations. Given:

data class CustomerDto(
    val id: String,
    val name: String,
    val whatever: String,
)

data class Customer(
    val id: CustomerID,
    val name: CustomerName,
    val whatever: CustomerWhatever,
)

Going from the first above to the second, I cannot do something like CustomerID(id).andThen { id -> ... } to build the final object. But I have to validate the CustomerID before passing it to the Customer constructor (since the CustomerID type enforces validity of the data it contains), and so I am left with no choice but to validate by hand the different elements, and compose the errors manually.

What is the suggested usage pattern for Konform to have both type-level guarantees of data validity when passing values around and composability of validations to build more complex objects ?

I hope I've managed to explain my question correctly, and I'm looking forward to your answer! Have a great day ๐Ÿ˜ƒ

Subclassing errors from ValidationError

Sometimes we need to attach additional meta-data to validation results (#48) and perform custom l10n for message (#2). Both tasks fits nicely into custom ValidationError types (also it is quite handy to get all possible error types for validation).

I have something like this in mind:

data class UserProfile(
    val fullName: String,
    val age: Int
)

sealed class UserValidationError: ValidationError

class AgeRestrictionValidationError(
    override val dataPath: String,
    userAge: Int,
    requiredAge: Int
) : UserValidationError {
    override val message = "User must be at least $requiredAge years, but found $userAge"
}

fun ValidationBuilder<User, UserValidationError>.minimumAge(ageRequirement: Int) {
    // Pass currentPath from constraint?
    addConstrains(AgeRestrictionValidationError(currentPath, it.age, ageRequirement)) { it.age > ageRequirement}
}

// Add returned errors superclass to generic parameter
val validator = Validation<User, UserValidationError> {
    UserProfile::age {
        minimumAge(18)
    }
}

Maybe there is a cleaner way to achieve this in DSL (for example - avoid overloading ValidationBuilder with custom validations).

Kotlin 2 & multiplatform support

With the upcoming Kotlin 2 release (RC2 is out right now), we should update the build and make this a proper multiplatform project that supports all the targets. Since this project only relies on the standard lib, there's no good reason to not do this.

  • Use the improved DSL for multiplatform
  • Add Native, Wasm, and all the other platforms
  • Preserve language compatibility with older Kotlin versions

Advise on how to structure data classes to produce a strongly typed validated data

I am trying to find a good way to validate data from Spring MVC implementing a REST POST endpoint. There are two obstacles with Kotlin... suppose we have this data class:

data class Message(
    val message: String,
    val level: System.Logger.Level //an enum
)

If either parameter is not supplied, Spring passes nulls and these cause a constructor violation before any validation can run, and secondly if the enum value is illegal again the data class does not get instantiated.

So I have to use a permissive data class like this:

data class Message(
   val message: String?,
   val level: String?
)

Konform can easily validate this using constraints like this:

    fun validate() = Validation<Message> {
        Message::message required {}
        Message::level required { enum<System.Logger.Level>() }
    }(this)

but still to use the data class, even though I know the data to be valid, I have to make calls like:

        request.message!!
        System.Logger.Level.valueOf(request.level!!)

it would be better to do something like the following which fully encapsulates all this permissive typing:

class Message(
    message: String?,
    level: String?
) {
    init {
        Validation<Message> {
            message required {}
            level required { enum<System.Logger.Level>() }
        }.validate(this)
    }
    val message = message!!
    val level = System.Logger.Level.valueOf(level!!)
}

and exposes the public properties of the non-optional String and non-optional enum value. However, this doesn't work, because the validator cannot bind to the constructor parameters.

Is there another way? - perhaps by using Validation<String> {.. on the fields one by one, but then a useful method like ValidationReseult.combineWith is not available to me.

Custom validations called with `run(...)` do not set a `dataPath` in the `ValidationResult`

    val validator = Validation<FormDto> {

      val validateDiscountedUntil = Validation<FormDto> {
        addConstraint("When specifying a discount until date, it should be in the future") {
          if (it.discountUntil == null) {
              true
          } else {
              it.discountUntil > LocalDate.now()
          }
        }
      }
      FormDto::discountUntil {
        run(validateDiscountedUntil)
      }
    }

Gives me:

Invalid(errors=[ValidationError(dataPath=, message=When specifying a discount until date, it should be in the future)])

I find this weird behavior as I do specify it on FormDto::discountUntil

Project abandoned?

Threre haven't been any updates for a year now. Issues are unresolved and untriaged
Should we migrate from this library?

Cross-field validation

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(???)
}

Getting all errors for a result

Currently a ValidationResult<T> can be used to get errors for a specific field:

result[UserProfile::fullName]

Is there any way to get a map of all errors for a given validation (not a separate list for every field)?

How to put a hint on an empty required block?

From the README it is not clear how I put a hint on an empty required block.

    val validator = Validation<DealFormDto> {
      DealFormDto::closedBy required {}
    }

I get the "is required" default message when closedBy is not present. But how to set my own message?

This does not work:

    val validator = Validation<DealFormDto> {
      DealFormDto::closedBy required { hint "Closed by is missing" }
    }

migrate away from bintray

with bintray and jcenter shutting down and service failures of bintray becoming more frequent
migrating the artifact to mavencentral
(or providing intructions on how to properly use it via jitpack as a temporary fix)
would be useful

Add 0.1.0 to MavenCentral

Hello!

We have installed version 0.1.0 in legacy project and want to make a simple switch from Bintray to MavenCentral, but MavenCentral does not have version 0.1.0

Could You pulblish it?

Add support for polymorphic list validation

Hello, I have one use-case that I cannot achieve with the library right now. I have a list of multiple common types and I want to have specific validation for each type. For example:

sealed interface Event {
    val id: ObjectId
    val start: Instant
    val end: Instant
    ... some other common fileds
}

data class ConcertEvent(
     val band: String,
     ... other fields
): Event

data class TheaterEvent(
     val stage: String,
     ... other fields
): Event

data class Subscribe(
    val events: List<Event>,
    ... other fields
);

Currently, if I want to add validation for Subscribe.events I can do this only by validating Event fields with

Subscribe.events onEach {
     here we cannot have validation for `TheaterEvent` or `ConcertEvent` fields only for `Event`
}

I think is going to be very useful if we have something like this

Subscribe.events onEach<ConcertEvent> {
    ConcertEvent::band required {}
}

Subscribe.events onEach<TheaterEvent> {
    TheaterEvent::stage required {}
   ... other validations
}

What do you think? Is it possible to achieve similar result? Do you think there would be a better approach?

PS. Konform is great! I love it.

Kotlintest/Kotest Matchers

Kotest (previously kotlintest) has added support for Konform testing.
https://github.com/kotest/kotest

Example:

   val validateUser = Validation<UserProfile> {
      UserProfile::fullName {
         minLength(2)
         maxLength(100)
      }

      UserProfile::age ifPresent {
         minimum(0)
         maximum(150)
      }
   }

      val user1 = UserProfile("Alice", 25)
      validateUser shouldBeValid user1

      val user2 = UserProfile("A", -1)
      validateUser.shouldBeInvalid(user2) {
         it.shouldContainError(UserProfile::fullName, "must have at least 2 characters")
         it.shouldContainError(UserProfile::age, "must be at least '0'")
      }

Would you like me to open a PR to add instructions to the readme?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.