Flame 0.1.0-alpha.9

This is a prerelease version of Flame.
dotnet add package Flame --version 0.1.0-alpha.9
                    
NuGet\Install-Package Flame -Version 0.1.0-alpha.9
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Flame" Version="0.1.0-alpha.9" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Flame" Version="0.1.0-alpha.9" />
                    
Directory.Packages.props
<PackageReference Include="Flame" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Flame --version 0.1.0-alpha.9
                    
#r "nuget: Flame, 0.1.0-alpha.9"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Flame@0.1.0-alpha.9
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Flame&version=0.1.0-alpha.9&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Flame&version=0.1.0-alpha.9&prerelease
                    
Install as a Cake Tool

Flame

Schema validation for F#. Parse, validate, and transform JSON in a single pass with zero-allocation performance.

Install

dotnet add package Flame --prerelease

Quick Start

Define an F# record type. Flame generates a schema automatically — option fields become optional, nested records and typed lists are handled recursively:

open Flame

type Address = { Street: string; City: string; Zip: string }
type Tag = { Key: string; Value: string }

type CreateUser = {
    Name: string                // required, rejects empty strings
    Email: string               // required, rejects empty strings
    Age: int                    // required
    Address: Address            // required nested object
    Tags: Tag list              // required list of nested objects
    Bio: string option          // optional, defaults to None
}

let userSchema = Schema.fromType<CreateUser>()

Parse JSON in one call:

let json = """
{
  "Name": "Alice",
  "Email": "alice@example.com",
  "Age": 30,
  "Address": { "Street": "123 Main St", "City": "Springfield", "Zip": "62701" },
  "Tags": [{ "Key": "role", "Value": "admin" }],
  "Bio": "Hello world"
}"""

match Schema.parseString userSchema json with
| Ok user -> printfn $"Hello, {user.Name} from {user.Address.City}"
| Error errors -> errors |> List.iter (printfn "  %s")

That's it. No boilerplate, no manual field mapping, no separate validation step. Flame handles required/optional detection, nested records, typed lists, and error reporting with dotted paths (Address.Zip is required).

A more complete example

A product catalog API with nested types, lists, and optional fields:

open Flame

// Domain types
type Dimension = { Width: float; Height: float; Depth: float; Unit: string }
type Image = { Url: string; Alt: string option }
type Variant = { Sku: string; Color: string; Size: string; Price: float; Stock: int }

type CreateProduct = {
    Name: string                    // required, rejects empty
    Description: string option      // optional
    Category: string                // required
    Dimensions: Dimension option    // optional nested object
    Images: Image list              // required list of nested objects
    Variants: Variant list          // required list of nested objects
    Draft: bool option              // optional, defaults to None
}

// Schema is generated once and cached
let productSchema = Schema.fromType<CreateProduct>()

// Parse a request body
let json = """
{
  "Name": "Standing Desk",
  "Category": "furniture",
  "Dimensions": { "Width": 120.0, "Height": 75.0, "Depth": 60.0, "Unit": "cm" },
  "Images": [
    { "Url": "https://cdn.example.com/desk-1.jpg", "Alt": "Front view" },
    { "Url": "https://cdn.example.com/desk-2.jpg" }
  ],
  "Variants": [
    { "Sku": "DESK-BLK-S", "Color": "black", "Size": "small", "Price": 399.99, "Stock": 12 },
    { "Sku": "DESK-WHT-L", "Color": "white", "Size": "large", "Price": 499.99, "Stock": 5 }
  ],
  "Draft": true
}"""

match Schema.parseString productSchema json with
| Ok product ->
    printfn $"{product.Name} — {product.Variants.Length} variants, {product.Images.Length} images"
    for v in product.Variants do
        printfn $"  {v.Sku}: ${v.Price} ({v.Stock} in stock)"
| Error errors ->
    printfn "Validation errors:"
    for e in errors do printfn $"  {e}"

If you need validation rules on top of the type structure, use the schema { } CE instead — see Adding Validation Rules.

Type mapping

F# Type Behavior
string Required. Rejects empty strings.
int, float, bool Required.
DateTime, DateTimeOffset Required. Parses ISO 8601.
string option, int option, etc. Optional. Defaults to None.
string list, int list, etc. Required typed list.
Record list Required. Each item parsed recursively.
Nested record Required nested object, parsed recursively.
Record option Optional nested object.

fromType works with both named records and anonymous records:

let schema = Schema.fromType<{| Name: string; Score: float option |}>()

Results are cached per type — reflection only runs on the first call.

Adding Validation Rules

When you need more than type-level validation — length limits, format checks, transforms — use the schema { } computation expression:

let createUserSchema = schema {
    let! name  = Schema.required "name"  Schema.string [ Schema.nonempty; Schema.maxLength 100; Schema.trim ]
    let! email = Schema.required "email" Schema.string [ Schema.email; Schema.trim; Schema.lowercase ]
    let! age   = Schema.optional "age"   Schema.int 0  [ Schema.min 0; Schema.max 150 ]
    return {| Name = name; Email = email; Age = age |}
}

Each let! binds a field with a name, a type parser, and a list of rules. The return assembles the typed result.

Required vs optional

// Required: must be present and non-null
let! title = Schema.required "title" Schema.string [ Schema.nonempty; Schema.maxLength 200 ]

// Optional: can be omitted or null, falls back to default. Rules apply when present.
let! priority = Schema.optional "priority" Schema.string "medium" [ Schema.enum' ["low"; "medium"; "high"] ]

Rule chaining

Rules are applied left to right. Each rule receives the output of the previous rule. Transforms should come before validators:

// 1. trim whitespace  2. check not empty  3. lowercase
let! email = Schema.required "email" Schema.string [ Schema.trim; Schema.nonempty; Schema.lowercase ]

If a rule fails, the error is collected but subsequent rules still run. Flame never short-circuits — all errors are reported.

String rules

Schema.minLength 3              // at least 3 characters
Schema.maxLength 200            // at most 200 characters
Schema.length 5                 // exactly 5 characters
Schema.nonempty                 // must not be empty
Schema.pattern @"^\d{5}$"      // must match regex
Schema.email                    // must contain @ and .
Schema.url                      // must start with http:// or https://
Schema.uuid                     // must be a valid UUID/GUID
Schema.ip                       // must be a valid IP address (v4 or v6)
Schema.ipv4                     // must be a valid IPv4 address
Schema.ipv6                     // must be a valid IPv6 address
Schema.datetime                 // must be a valid ISO 8601 date/time string
Schema.startsWith "https"       // must start with prefix
Schema.endsWith ".com"          // must end with suffix
Schema.includes "@"             // must contain substring
Schema.enum' ["a"; "b"; "c"]   // must be one of the listed values

String transforms

Transforms modify the value. They always succeed and pass the transformed value to the next rule.

Schema.trim                     // remove leading/trailing whitespace
Schema.lowercase                // convert to lowercase (invariant)
Schema.uppercase                // convert to uppercase (invariant)

Number rules

Schema.min 0.0                  // >= 0 (inclusive)
Schema.max 100.0                // <= 100 (inclusive)
Schema.gt 0.0                   // > 0 (exclusive)
Schema.lt 100.0                 // < 100 (exclusive)
Schema.positive                 // > 0
Schema.negative                 // < 0
Schema.nonnegative              // >= 0
Schema.nonpositive              // <= 0
Schema.int'                     // must be a whole number (e.g. 5.0 ok, 5.5 fails)
Schema.multipleOf 0.5           // must be divisible by 0.5

Array rules

Applied to the parsed list after all items have been individually validated:

Schema.minItems 1               // at least 1 item
Schema.maxItems 10              // at most 10 items
Schema.nonEmpty                 // must have at least 1 item

Type Parsers

Flame provides parsers for common types. Each parser handles coercion from strings automatically — "42" parses as int, "true" as bool, etc. Coercion is consistent across all parsing paths.

Parser F# Type Accepts
Schema.string string JSON strings
Schema.int int JSON numbers, numeric strings ("42")
Schema.float float JSON numbers, numeric strings ("3.14")
Schema.bool bool JSON true/false, strings "true"/"false"
Schema.dateTime DateTime ISO 8601 strings
Schema.dateTimeOffset DateTimeOffset ISO 8601 with offset
Schema.list parser 'T list JSON arrays
Schema.nullable parser 'T option JSON null becomes None
Schema.nest schema nested object JSON objects

Lists

let! tags   = Schema.required "tags"   (Schema.list Schema.string) [ Schema.nonEmpty; Schema.maxItems 10 ]
let! scores = Schema.required "scores" (Schema.list Schema.int) []

Nullable fields

let! note = Schema.required "note" (Schema.nullable Schema.string) []
// note : string option — JSON null becomes None, a string becomes Some "..."

Lists of nested objects

let itemSchema = schema {
    let! name = Schema.required "name" Schema.string [ Schema.nonempty ]
    let! qty  = Schema.required "qty"  Schema.int    [ Schema.positive ]
    return {| Name = name; Qty = qty |}
}

let orderSchema = schema {
    let! items = Schema.required "items" (Schema.list (Schema.nest itemSchema)) [ Schema.nonEmpty ]
    return {| Items = items |}
}

Nested Schemas

Compose schemas with Schema.nest to validate nested JSON objects:

let addressSchema = schema {
    let! street = Schema.required "street" Schema.string [ Schema.nonempty ]
    let! city   = Schema.required "city"   Schema.string [ Schema.nonempty ]
    let! zip    = Schema.required "zip"    Schema.string [ Schema.pattern @"^\d{5}$" ]
    return {| Street = street; City = city; Zip = zip |}
}

let userSchema = schema {
    let! name    = Schema.required "name"    Schema.string [ Schema.nonempty ]
    let! address = Schema.required "address" (Schema.nest addressSchema) []
    return {| Name = name; Address = address |}
}

Errors from nested schemas use dotted paths: address.zip: must match pattern ^\d{5}$

Cross-field Validation

Use Schema.check with do! to validate relationships between fields:

let dateRangeSchema = schema {
    let! startDate = Schema.required "start" Schema.dateTime []
    let! endDate   = Schema.required "end"   Schema.dateTime []
    do! Schema.check (fun () ->
        if endDate > startDate then Ok ()
        else Error "end: must be after start"
    )
    return {| Start = startDate; End = endDate |}
}

Multiple checks can be chained:

let registrationSchema = schema {
    let! password = Schema.required "password" Schema.string [ Schema.minLength 8 ]
    let! confirm  = Schema.required "confirm"  Schema.string []
    do! Schema.check (fun () ->
        if password = confirm then Ok () else Error "confirm: must match password"
    )
    return {| Password = password |}
}

Note: schemas with Schema.check use the element parsing path instead of the zero-alloc buffer path, since cross-field checks use closures that capture field values.

Parsing

All parse functions return Result<'T, string list>.

Function Input Notes
Schema.parseString string Default. Uses Utf8JsonReader internally.
Schema.parseBuffer ReadOnlySequence<byte> Zero-alloc. Best for Kestrel request bodies.
Schema.parseJson JsonElement When you already have a JsonDocument.
Schema.parseStream Stream Async. For request body streams.
Schema.parsePipe PipeReader Async. For Kestrel's PipeReader.
Schema.parseLookup string -> string option Zero-alloc. For query strings, route params, form data.
Schema.parseMap IReadOnlyDictionary<string, string> Delegates to parseLookup.

Two parsing paths

Flame has two internal paths. The buffer path (parseString, parseBuffer) parses directly from raw bytes using Utf8JsonReader with ArrayPool<obj> — no JsonDocument allocated. The element path (parseJson, parseStream) works with JsonElement. Schemas with Schema.check fall back to the element path.

Parsing from query strings and forms

parseLookup skips JSON entirely — values are coerced from strings to the target type:

let paginationSchema = schema {
    let! page  = Schema.optional "page"  Schema.int 1  [ Schema.positive ]
    let! limit = Schema.optional "limit" Schema.int 20 [ Schema.min 1; Schema.max 100 ]
    return {| Page = page; Limit = limit |}
}

let result = Schema.parseLookup paginationSchema (fun name ->
    match name with
    | "page" -> Some "2"
    | "limit" -> Some "50"
    | _ -> None
)

Error Handling

Flame collects all validation errors — it never short-circuits. This lets users fix all problems at once.

match Schema.parseString mySchema """{"name":"","email":"bad","age":-1}""" with
| Error errors ->
    // [
    //   "name: must not be empty"
    //   "email: invalid email format"
    //   "age: must be at least 0"
    // ]
Situation Format Example
Field validation "field: message" "email: invalid email format"
Missing required field "field is required" "name is required"
Nested field "parent.field: message" "address.zip: must match pattern ^\d{5}$"
List item "field.[index]: message" "items.[2]: expected integer"

JSON Schema Generation

Generate standard JSON Schema from any Flame schema:

let jsonSchema = Schema.toJsonSchema createUserSchema
{
  "type": "object",
  "properties": {
    "name": { "type": "string", "minLength": 1, "maxLength": 100 },
    "email": { "type": "string", "format": "email" },
    "age": { "type": "integer", "minimum": 0, "maximum": 150 }
  },
  "required": ["name", "email"]
}

All validators emit correct JSON Schema keywords: minLength, maxLength, pattern, minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf, minItems, maxItems, format, enum. Nested objects include properties and required. Arrays include items.

Validating Existing Values

Use validator { } to validate an F# value that already exists — no JSON involved:

let userValidator = validator {
    validate "Name" (fun (u: User) -> u.Name) [ Schema.nonempty; Schema.maxLength 100 ]
    validate "Email" (fun u -> u.Email) [ Schema.email ]
    validate "Address.Zip" (fun u -> u.Address.Zip) [ Schema.pattern @"^\d{5}$" ]
}

match Validator.validate userValidator someUser with
| Ok user -> // valid
| Error errors -> // ["Name: must not be empty"; "Address.Zip: must match pattern ..."]

Uses the same rules as schema { } but works directly on typed values via lambdas. No JSON, no reflection at runtime.

Validator.fromType auto-generates a validator from a record type (required strings get nonempty, nested records recurse):

let userValidator = Validator.fromType<User>()

Validate and Transform

Use validated { } to validate an input and produce a different output type in one step. Transforms (trim, lowercase) are applied to the output values:

type CreateUserInput = { Name: string; Email: string; Age: int }
type ValidUser = { Name: string; Email: string; Age: int }

let createUser = validated {
    let! name  = Validated.field "Name"  (fun (r: CreateUserInput) -> r.Name)  [ Schema.nonempty; Schema.trim ]
    let! email = Validated.field "Email" (fun r -> r.Email) [ Schema.email; Schema.trim; Schema.lowercase ]
    let! age   = Validated.field "Age"   (fun r -> r.Age)   [ Schema.min 0; Schema.max 150 ]
    return { ValidUser.Name = name; Email = email; Age = age }
}

match Validated.run createUser { Name = "  Alice  "; Email = "  ALICE@TEST.COM  "; Age = 30 } with
| Ok user -> // { Name = "Alice"; Email = "alice@test.com"; Age = 30 }
| Error errors -> // all validation errors collected

Domain-Driven Design with value objects

The validated { } CE works naturally with single-case discriminated unions for domain modeling. Validation and domain construction happen in one step:

// Value objects
type Name = Name of string
type Email = Email of string
type Age = Age of int

// Domain model
type ValidUser = { Name: Name; Email: Email; Age: Age }

// Input DTO
type CreateUserRequest = { Name: string; Email: string; Age: int }

let createUser = validated {
    let! name  = Validated.field "Name"  (fun (r: CreateUserRequest) -> r.Name)  [ Schema.nonempty; Schema.maxLength 100; Schema.trim ]
    let! email = Validated.field "Email" (fun r -> r.Email) [ Schema.email; Schema.trim; Schema.lowercase ]
    let! age   = Validated.field "Age"   (fun r -> r.Age)   [ Schema.positive; Schema.max 150 ]
    return { Name = Name name; Email = Email email; Age = Age age }
}

// DTO in, validated domain model out:
match Validated.run createUser request with
| Ok user -> // user.Email = Email "alice@test.com"
| Error errors -> // ["Email: invalid email format"; "Age: must be positive"]

Performance

Flame parses and validates in a single pass using Utf8JsonReader with ArrayPool<obj>. No intermediate JsonDocument on the buffer path.

Benchmarks on Apple M4 Pro, .NET 10:

Scenario Flame (buffer) FluentValidation (STJ + validate) Speedup Memory
3 fields + rules 239 ns / 408 B 266 ns / 832 B 1.1x 2x less
10 fields + nested 763 ns / 1,448 B 1,098 ns / 2,856 B 1.4x 2x less
Nested objects 370 ns / 824 B 509 ns / 2,136 B 1.4x 2.6x less
uuid + ip + array validators 516 ns / 944 B 566 ns / 1,800 B 1.1x 1.9x less
dotnet run --project benchmarks/Flame.Benchmarks -c Release

License

MIT

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Flame:

Package Downloads
Firefly.Server

A minimal F# web framework built on Kestrel

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.0-alpha.9 62 3/31/2026
0.1.0-alpha.8 35 3/31/2026
0.1.0-alpha.6 37 3/31/2026
0.1.0-alpha.5 40 3/31/2026
0.1.0-alpha.4 41 3/31/2026
0.1.0-alpha.3 43 3/31/2026
0.1.0-alpha.2 45 3/31/2026
0.1.0-alpha 42 3/31/2026