Awesome
AccidentalFish.FSharp.Validation
A simple and extensible declarative validation framework for F#.
A sample console app is available demonstrating basic usage.
For issues and help please log them in the Issues area or contact me on Twitter.
You can also check out my blog for other .NET projects, articles, and cloud architecture and development.
Getting Started
Add the NuGet package AccidentalFish.FSharp.Validation to your project.
Consider the following model:
open AccidentalFish.FSharp.Validation
type Order = {
id: string
title: string
cost: double
}
We can declare a validator for that model as follows:
let validateOrder = createValidatorFor<Order>() {
validate (fun o -> o.id) [
isNotEmpty
hasLengthOf 36
]
validate (fun o -> o.title) [
isNotEmpty
hasMaxLengthOf 128
]
validate (fun o -> o.cost) [
isGreaterThanOrEqualTo 0.
]
}
The returned validator is a simple function that can be executed as follows:
let order = {
id = "36467DC0-AC0F-43E9-A92A-AC22C68F25D1"
title = "A valid order"
cost = 55.
}
let validationResult = order |> validateOrder
The result is a discriminated union and will either be Ok for a valid model or Errors if issues were found. In the latter case this will be of type ValidationItem list. ValidationItem contains three properties:
Property | Description |
---|---|
errorCode | The error code, typically the name of the failed validation rule |
message | The validation message |
property | The path of the property that failed validation |
The below shows an example of outputting errors to the console:
match validationResult with
| Ok -> printf "No validation errors\n\n"
| Errors errors -> printf "errors = %O" e
Validating Complex Types
Often your models will contain references to other record types and collections. Take the following model as an example:
type OrderItem =
{
productName: string
quantity: int
}
type Customer =
{
name: string
}
type Order =
{
id: string
customer: Customer
items: OrderItem list
}
First if we look at validating the customer name we can do this one of two ways. Firstly we can simply express the full path to the customer name property:
let validateOrder = createValidatorFor<Order>() {
validate (fun o -> o.customer.name) {
isNotEmpty
hasMaxLengthOf 128
}
}
Or, if we want to reuse the customer validations, we can combine validators:
let validateCustomer = createValidatorFor<Customer>() {
validate (fun c -> c.name) {
isNotEmpty
hasMaxLengthOf 128
}
}
let validateOrder = createValidatorFor<Order>() {
validate (fun o -> o.customer) {
withValidator validateCustomer
}
}
In both cases above the property field in the error items will be fully qualified e.g.:
customer.name
Validating items in collections are similar - we simply need to supply a validator for the items in the collection as shown below:
let validateOrderItem = createValidatorFor<OrderItem>() {
validate (fun i -> i.productName) {
isNotEmpty
hasMaxLengthOf 128
}
validate (fun i -> i.quantity) {
isGreaterThanOrEqualTo 1
}
}
let validateOrder = createValidatorFor<Order>() {
validate (fun o -> o.items) {
isNotEmpty
eachItemWith validateOrderItem
}
}
Again the property fields in the error items will be fully qualified and contain the index e.g.:
items.[0].productName
Conditional Validation
I'm still playing around with this a little but doc's as it stands now
Using validateWhen
If you just have conditional logic that applies to one or two properties validateWhen can be used to specifiy which per property validations to use under which conditions. Given the order model below:
type DiscountOrder = {
value: int
discountPercentage: int
}
If we want to apply different validations to discountPercentage then we can do so using validateWhen as shown here:
let discountOrderValidator = createValidatorFor<DiscountOrder>() {
validateWhen (fun w -> w.value < 100) (fun o -> o.discountPercentage) [
isEqualTo 0
]
validateWhen (fun w -> w.value >= 100) (fun o -> o.discountPercentage) [
isEqualTo 10
]
validate (fun o -> o.value) [
isGreaterThan 0
]
}
This will always validate that the value of the order is greater than 0. If the value is less than 100 it will ensure that the discount percentage is 0 and if the value is greater than or equal to 100 it will ensure the discount percentage is 10.
Using withValidatorWhen
This validateWhen approach is fine if you have only single properties but if you have multiple properties bound by a condition then can result in a lot of repetition. In this scenario using withValidatorWhen can be a better approach. Lets extend our order model to include an explanation for a discount - that we only want to be set when the discount is set:
type DiscountOrder = {
value: int
discountPercentage: int
discountExplanation: string
}
Now we'll declare three validators:
let orderWithDiscount = createValidatorFor<DiscountOrder>() {
validate (fun o -> o.discountPercentage) [
isEqualTo 10
]
validate (fun o -> o.discountExplanation) [
isNotEmpty
]
}
let orderWithNoDiscount = createValidatorFor<DiscountOrder>() {
validate (fun o -> o.discountPercentage) [
isEqualTo 0
]
validate (fun o -> o.discountExplanation) [
isEmpty
]
}
let discountOrderValidator = createValidatorFor<DiscountOrder>() {
validate (fun o -> o) [
withValidatorWhen (fun o -> o.value < 100) orderWithNoDiscount
withValidatorWhen (fun o -> o.value >= 100) orderWithDiscount
]
validate (fun o -> o.value) [
isGreaterThan 0
]
}
The above can also be expressed more concisely in one block:
let validator = createValidatorFor<DiscountOrder>() {
validate (fun o -> o) [
withValidatorWhen (fun o -> o.value < 100) (createValidatorFor<DiscountOrder>() {
validate (fun o -> o) [
withValidatorWhen (fun o -> o.value < 100) orderWithNoDiscount
withValidatorWhen (fun o -> o.value >= 100) orderWithDiscount
]
validate (fun o -> o.value) [
isGreaterThan 0
]
})
withValidatorWhen (fun o -> o.value >= 100) (createValidatorFor<DiscountOrder>() {
validate (fun o -> o.discountPercentage) [
isEqualTo 10
]
validate (fun o -> o.discountExplanation) [
isNotEmpty
]
})
]
validate (fun o -> o.value) [
isGreaterThan 0
]
}
Using A Function
If your validation is particularly complex then you can simply use a function or custom validator (though you might want to consider if this kind of logic is best expressed in a non-declarative form).
Custom validators are described in a section below. A function example follows:
type DiscountOrder = {
value: int
discountPercentage: int
discountExplanation: string
}
let validator = createValidatorFor<DiscountOrder>() {
validate (fun o -> o) [
withFunction (fun o ->
match o.value < 100 with
| true -> Ok
| false -> Errors([
{
errorCode="greaterThanEqualTo100"
message="Some error"
property = "value"
}
])
)
]
}
Discriminated Unions
Single Case
Its common to use single case unions for wrapping simple types and preventing, for example, misassignment. Consider the following model:
type CustomerId = CustomerId of string
type Customer =
{
customerId: CustomerId
}
We might want to ensure the customer ID value is not empty and has a maximum length. One way to accomplish that would be to use a function (see Collections above) but the framework also has a validate command that supports unwrapping the value as shown below:
let unwrapCustomerId (CustomerId id) = id
let validator = createValidatorFor<Customer>() {
validateSingleCaseUnion (fun c -> c.id) unwrapCustomerId [
isNotEmpty
hasMaxLengthOf 10
]
}
For an excellent article on single case union types see F# for Fun and Profit.
Multiple Case
We can handle multiple case discriminated unions using the validateUnion command. Consider the following model:
type MultiCaseUnion =
| NumericValue of double
| StringValue of string
type UnionExample =
{
value: MultiCaseUnion
}
To validate the contents of the union we need to unwrap and apply the appropriate validators based on the union case which we can do as shown below:
let unionValidator = createValidatorFor<UnionExample>() {
validateUnion (fun o -> o.value) (fun v -> match v with | StringValue s -> Unwrapped(s) | _ -> Ignore) [
isNotEmpty
hasMinLengthOf 10
]
validateUnion (fun o -> o.value) (fun v -> match v with | NumericValue n -> Unwrapped(n) | _ -> Ignore) [
isGreaterThan 0.
]
}
Essentially the validateUnion command takes a parameter that supports a match and it, itself, returns a discriminated union. Return Unwrapped(value) to have the validation block run on the unwrapped value or return Ignore to have it skip that.
Option Types
To deal with option types in records use validateRequired, validateUnrequired, validateRequiredWhen and validateUnrequiredWhen instead of the already introduced validate and validateWhen commands.
validateRequired and validateRequiredWhen will apply the validators if the option type is Some. If the option type is None then a validation error will be generated.
validateUnrequired and validateUnrequiredWhen will apply the validators if the option type is Some but if the option type is None it will not generate a validation error, it simply won't run the validators.
Built-in Validators
The library includes a number of basic value validators (as seen in the examples above):
Validator | Description |
---|---|
isEqualTo expected | Is the tested value equal to the expected value |
isNotEqualTo unexpected | Is the tested value not equal to the unexpected value |
isGreaterThan value | Is the tested value greater than value |
isGreaterThanOrEqualTo minValue | Is the tested value greater than or equal to minValue |
isLessThan value | Is the tested value less than value |
isLessThanOrEqualTo maxValue | Is the tested value less than or equal to maxValue |
isEmpty | Is the tested value empty |
isNotEmpty | Is the sequence (including a string) not empty |
isNotNull | Ensure the value is not null |
eachItemWith validator | Apply validator to each item in a sequence |
hasLengthOf length | Is the sequence (including a string) of length length |
hasMinLengthOf length | Is the sequence (including a string) of a minimum length of length |
hasMaxLengthOf length | Is the sequence (including a string) of a maximum length of length |
isNotEmptyOrWhitespace value | Is the tested value not empty and not whitespace |
withValidator validator | Applies the specified validator to the property. Is an alias of withFunction |
withValidatorWhen predicate validator | Applies the specified validator when a condition is met. See conditional validations above. |
withFunction function | Apples the given function to the property. The function must have a signature of 'validatorTargetType -> ValidationState |
I'll expand on this set over time. In the meantime it is easy to add additional validators as shown below.
Adding Custom Validators
Its easy to add custom validators as all they are are functions with the signature string -> 'a -> ValidationState. The first parameter is the name of the property that the validator is being applied to and the second the value. We then return the validation state.
For example lets say we want to write a validator function for a discriminated union and a model that uses it:
type TrafficLightColor = | Red | Green | Blue
type TrafficLight =
{
color: TrafficLightColor
}
To check if the traffic light is green we could write a validator as follows:
let isGreen propertyName value =
match value with
| Green -> Ok
| _ -> Errors([{ errorCode="isGreen" ; message="The light was not green" ; property = propertyName }])
And we could use it like any other validator:
let trafficLightValidator = createValidatorFor<TrafficLight>() {
validate (fun r -> r.color) [
isGreen
]
}
If we want to be able to supply parameters to the validator then we need to write a function that returns our validator function. For example if we want to be able to specify the color we could write a validator as follows:
let isColor color =
let comparator propertyName value =
match value = color with
| true -> Ok
| false -> Errors([{ errorCode="isColor" ; message=sprintf "The light was not %O" value ; property = propertyName }])
comparator
And then we can use it like any other validator:
let trafficLightValidator = createValidatorFor<TrafficLight>() {
validate (fun r -> r.color) [
isColor Amber
]
}