FireflyFirefly
DocsGuides
GitHub

Parsing & Errors

How Flame turns JSON, byte buffers, streams, query strings, and forms into typed values — and how it reports every error at once.

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.

ParserF# TypeAccepts
Schema.stringstringJSON strings
Schema.intintJSON numbers, numeric strings ("42")
Schema.floatfloatJSON numbers, numeric strings ("3.14")
Schema.boolboolJSON true/false, strings "true"/"false"
Schema.dateTimeDateTimeISO 8601 strings
Schema.dateTimeOffsetDateTimeOffsetISO 8601 with offset
Schema.list parser'T listJSON arrays
Schema.nullable parser'T optionJSON null becomes None
Schema.nest schemanested objectJSON 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>.

FunctionInputNotes
Schema.parseStringstringDefault. Uses Utf8JsonReader internally.
Schema.parseBufferReadOnlySequence<byte>Zero-alloc. Best for Kestrel request bodies.
Schema.parseJsonJsonElementWhen you already have a JsonDocument.
Schema.parseStreamStreamAsync. For request body streams.
Schema.parsePipePipeReaderAsync. For Kestrel's PipeReader.
Schema.parseLookupstring -> string optionZero-alloc. For query strings, route params, form data.
Schema.parseMapIReadOnlyDictionary<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"
    // ]
SituationFormatExample
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"