SkillAgentSearch skills...

AccidentalFish.FSharp.Validation

Simple validator DSL / library for F#

Install / Use

/learn @JamesRandall/AccidentalFish.FSharp.Validation
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

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 suppor

View on GitHub
GitHub Stars89
CategoryDevelopment
Updated23d ago
Forks10

Languages

F#

Security Score

95/100

Audited on Mar 10, 2026

No findings