FireflyFirefly
DocsGuides
GitHub

Validation

Firefly integrates with Flame, a schema validation library for F#. Flame provides type-safe parsing, validation rules, and JSON Schema generation.

Defining Schemas

Use the schema computation expression to define schemas:

open Flame
 
type CreateUser = { Name: string; Email: string; Age: int }
 
let createUserSchema = schema<CreateUser> {
    required "name"  Schema.string [ Rules.minLength 1; Rules.maxLength 100; Rules.trim ]
    required "email" Schema.string [ Rules.email ]
    required "age"   Schema.int   [ Rules.min 0; Rules.max 150 ]
}

Field Types

Flame supports these built-in field parsers:

ParserF# TypeJSON Type
Schema.stringstringstring
Schema.intintnumber/string
Schema.floatfloatnumber/string
Schema.boolbooltrue/false/string
Schema.dateTimeDateTimestring
Schema.dateTimeOffsetDateTimeOffsetstring
Schema.list Schema.stringstring listarray
Schema.nullable Schema.stringstring optionstring/null

Required vs Optional Fields

type Profile = { Name: string; Bio: string option }
 
let profileSchema = schema<Profile> {
    required "name" Schema.string [ Rules.nonempty ]
    optional "bio"  Schema.string []           // defaults to None if missing
}

Optional fields with defaults:

type SearchParams = { Query: string; Page: int; Limit: int }
 
let searchSchema = schema<SearchParams> {
    required "query" Schema.string []
    withDefault "page"  Schema.int [] 1
    withDefault "limit" Schema.int [] 20
}

Validation Rules

Flame includes a comprehensive set of typed rules:

String Rules

RuleDescription
Rules.minLength nMinimum string length
Rules.maxLength nMaximum string length
Rules.length nExact string length
Rules.nonemptyMust not be empty
Rules.pattern "regex"Must match regex pattern
Rules.emailMust be a valid email
Rules.urlMust start with http:// or https://
Rules.uuidMust be a valid UUID
Rules.ipMust be a valid IP address
Rules.ipv4Must be a valid IPv4 address
Rules.ipv6Must be a valid IPv6 address
Rules.datetimeMust be a valid date/time string
Rules.oneOf ["a"; "b"]Must be one of the listed values
Rules.startsWith "prefix"Must start with prefix
Rules.endsWith "suffix"Must end with suffix
Rules.includes "sub"Must contain substring

Transform Rules

RuleDescription
Rules.trimTrim whitespace (applied before other rules)
Rules.lowercaseConvert to lowercase
Rules.uppercaseConvert to uppercase

Number Rules

RuleDescription
Rules.min nMinimum value (inclusive)
Rules.max nMaximum value (inclusive)
Rules.gt nGreater than (exclusive)
Rules.lt nLess than (exclusive)
Rules.positiveMust be > 0
Rules.negativeMust be < 0
Rules.nonnegativeMust be >= 0
Rules.nonpositiveMust be <= 0
Rules.multipleOf nMust be a multiple of n
Rules.integerFloat must be a whole number

Array Rules

RuleDescription
Rules.minItems nMinimum array length
Rules.maxItems nMaximum array length
Rules.nonEmptyMust have at least one item

Nested Schemas

Compose schemas for nested objects:

type Address = { Street: string; City: string; Zip: string }
type CreateOrder = { Customer: string; Address: Address }
 
let addressSchema = schema<Address> {
    required "street" Schema.string [ Rules.nonempty ]
    required "city"   Schema.string [ Rules.nonempty ]
    required "zip"    Schema.string [ Rules.pattern "^\\d{5}$" ]
}
 
let orderSchema = schema<CreateOrder> {
    required "customer" Schema.string [ Rules.nonempty ]
    required "address"  (Schema.nest addressSchema) []
}

Errors from nested schemas use dotted paths: "address.zip: must match pattern".

Using Schemas in Firefly

Manual Parsing

Parse the request body with Schema.parse (auto-detects JSON vs form data):

let createUser (req: Request) = task {
    match! Schema.parse createUserSchema req with
    | Ok user ->
        // user is a typed CreateUser record
        return Response.json user |> Response.status 201
    | Error errors ->
        // errors is string list
        return Response.json {| errors = errors |} |> Response.status 400
}

Validated Handler

Use Schema.validated to wrap a handler with automatic parsing and error responses:

let createUser =
    Schema.validated createUserSchema (fun user -> task {
        // `user` is already parsed and validated
        return Response.json user |> Response.status 201
    })
 
Route.post "/users" createUser

On validation failure, responds with 400 and { "errors": ["name: must be at least 1 character", ...] }.

Parsing Specific Sources

// JSON body only (via PipeReader — zero-alloc buffer path)
Schema.parseRequest schema req
 
// Form data only
Schema.parseFormRequest schema req
 
// Route parameters
Schema.parseParams schema req
 
// Query string
Schema.parseQuery schema req

Simple Validators

For cases where you do not need full schema parsing, use Validate:

let validateUser =
    Validate.combine [
        Validate.required "name" (fun u -> u.Name)
        Validate.minLength "name" 2 (fun u -> u.Name)
        Validate.maxLength "name" 100 (fun u -> u.Name)
        Validate.pattern "email" @"^[^@]+@[^@]+\.[^@]+$" (fun u -> u.Email)
    ]
 
let handler (req: Request) = task {
    let! user = req.Json<CreateUser>()
    match validateUser user with
    | Ok _ -> return Response.json user |> Response.status 201
    | Error errors -> return Response.json {| errors = errors |} |> Response.status 400
}