Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result
open Reified.Schema.JsonParsing
Reified.Parse changes serialized text into primitive typed values. "42" becomes 42, "true" becomes true. The
point of the package is not that it converts — Int32.TryParse converts — but that when conversion fails it says which
of three different things went wrong, in a value you can act on.
dotnet add package Reified.Parse
open Reified
Parse.int "12" // Ok 12
Parse.int "" // Error (MissingValue "int")
Parse.int "twelve" // Error (InvalidFormat ("int", "twelve"))
Parse.int "99999999999" // Error (OutOfRange ("int", "99999999999"))
ReifiedReified.ParseModulePrimitive parsers for untrusted serialized input.
int: string -> Result<int,ParseError>Parses a 32-bit integer.
ParseError is an independent leaf type. It carries no prose and no culture, so it stays comparable data you can map
into your own error case, assert on in a test, or pass across a boundary.
The three cases
type ParseError =
| MissingValue of target: string
| InvalidFormat of target: string * input: string
| OutOfRange of target: string * input: string
05-parsing__index.md_page.ParseErrorMissingValuetarget: stringstringAn abbreviation for the CLI type . Basic Types
InvalidFormatinput: stringOutOfRange| Case | What it means | Example |
|---|---|---|
MissingValue target |
The input was absent, empty, or only whitespace. There was nothing to convert. | Parse.int " " → MissingValue "int" |
InvalidFormat (target, input) |
Text was supplied but does not spell a value of that type. | Parse.bool "yes" → InvalidFormat ("bool", "yes") |
OutOfRange (target, input) |
Text spells a well-formed number the destination type cannot hold. | Parse.int "99999999999" → OutOfRange ("int", "99999999999") |
target names the destination type — "int", "bool", "decimal", "Guid" — so a caller can tell which conversion
failed without tracking it separately. input is the offending text, retained for redisplay.
The distinction between the three matters more than it looks. An empty field and a misspelled one usually deserve
different messages, and only OutOfRange tells a user that their number was understood but too big.
The parsers
| Parser | Returns | Accepts |
|---|---|---|
Parse.int |
int |
Optionally signed digits, invariant culture. |
Parse.long |
int64 |
As int, with the wider range. |
Parse.decimal |
decimal |
Digits with an optional sign, decimal point, and thousands separators, invariant culture. |
Parse.float |
float |
As decimal, plus exponent notation. |
Parse.bool |
bool |
"true" or "false", any casing. Not "1", "yes", or "on". |
Parse.guid |
System.Guid |
Any format Guid.TryParse accepts, including braced and hyphenless. |
Parse.dateTime |
System.DateTime |
Invariant-culture date and time text, such as "2026-03-04T09:30:00". |
Parse.dateTimeOffset |
System.DateTimeOffset |
As dateTime, with an offset such as "+10:00". |
Parse.dateOnly |
System.DateOnly |
Invariant-culture date text. .NET 8+ only. |
Parse.timeOnly |
System.TimeOnly |
Invariant-culture time text. .NET 8+ only. |
Parse.enum<'enum> |
'enum |
A case name, case-insensitive, or its numeric text. |
Every parser takes string and returns Result<'value, ParseError>. Every numeric parser uses invariant culture, so
the same text parses the same way on every machine — a decimal point is always ., never ,.
Only the numeric parsers can produce OutOfRange; the others report MissingValue or InvalidFormat.
The three failures in practice
// Missing: nothing was supplied.
Parse.decimal ""
// Error (MissingValue "decimal")
// Malformed: something was supplied, but it is not a decimal.
Parse.decimal "19,95 AUD"
// Error (InvalidFormat ("decimal", "19,95 AUD"))
// Out of range: well-formed, but too large for the type.
Parse.int "2147483648"
// Error (OutOfRange ("int", "2147483648"))
Reified.ParseModulePrimitive parsers for untrusted serialized input.
decimal: string -> Result<decimal,ParseError>Parses a decimal number.
int: string -> Result<int,ParseError>Parses a 32-bit integer.
Match on the case when the three need different treatment:
let describe error =
match error with
| MissingValue target -> $"Please supply a {target}."
| InvalidFormat (_, input) -> $"'{input}' is not in the expected format."
| OutOfRange (_, input) -> $"'{input}' is too large."
describe: ParseError -> stringerror: ParseErrorMissingValuetarget: stringInvalidFormatinput: stringOutOfRangeHand off to your own error type
Most call sites do not keep ParseError. Map it into the application's vocabulary at the point of use, and the
signature stops mentioning Reified at all:
type PortError = PortNotANumber of string
let port raw : Result<int, PortError> =
Parse.int raw |> Result.mapError (fun _ -> PortNotANumber raw)
05-parsing__index.md_page.PortErrorPortNotANumberstringAn abbreviation for the CLI type . Basic Types
port: string -> Result<int,PortError>raw: stringMicrosoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
intAn abbreviation for the CLI type . Basic Types
Reified.ParseModulePrimitive parsers for untrusted serialized input.
int: string -> Result<int,ParseError>Parses a 32-bit integer.
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Reified.Result.ResultModuleFail-fast helpers over the standard F# Result type.
mapError: ('a -> 'b) -> Result<'c,'a> -> Result<'c,'b>Maps the error value of a result.
Result.orError from Reified.Result is shorter when the error case carries nothing:
Parse.int raw |> Result.orError PortMissingKeep the ParseError instead — Result.mapError InvalidInteger into a case that carries it — when something later
needs to tell the three failures apart or redisplay the offending text.
When the text is one field of structured input rather than a standalone value, do not do this by hand.
Schema parses each field, accumulates every failure, and reports the
field path alongside it.
Optional input
Parse.optional distinguishes an absent value from a bad one. Absence succeeds as None; malformed present text still
fails.
Parse.optional Parse.int None
// Ok None
Parse.optional Parse.int (Some "42")
// Ok (Some 42)
Parse.optional Parse.int (Some "bad")
// Error (InvalidFormat ("int", "bad"))
Reified.ParseModulePrimitive parsers for untrusted serialized input.
optional: ('raw -> Result<'value,'error>) -> 'raw option -> Result<'value option,'error>Parses an optional input, preserving a present input's parsing failure. Parse.optional Parse.int None = Ok None Parse.optional Parse.int (Some "42") = Ok (Some 42) Parse.optional Parse.int (Some "bad") = Error (ParseError.InvalidFormat ("int", "bad"))
int: string -> Result<int,ParseError>Parses a 32-bit integer.
NoneThe representation of "No value"
SomeThe representation of "Value of type 'T" The input value. An option representing the value.
Parse.optionalOr supplies a fallback only when the input is absent:
Parse.optionalOr 80 Parse.int None
// Ok 80
Parse.optionalOr 80 Parse.int (Some "443")
// Ok 443
Parse.optionalOr 80 Parse.int (Some "bad")
// Error (InvalidFormat ("int", "bad"))
Reified.ParseModulePrimitive parsers for untrusted serialized input.
optionalOr: 'value -> ('raw -> Result<'value,'error>) -> 'raw option -> Result<'value,'error>Parses an optional input, using the supplied fallback only when the input is absent. Parse.optionalOr 80 Parse.int None = Ok 80 Parse.optionalOr 80 Parse.int (Some "443") = Ok 443 Parse.optionalOr 80 Parse.int (Some "bad") = Error (ParseError.InvalidFormat ("int", "bad"))
int: string -> Result<int,ParseError>Parses a 32-bit integer.
NoneThe representation of "No value"
SomeThe representation of "Value of type 'T" The input value. An option representing the value.
The fallback never recovers from malformed input. A default is for an omitted setting, not a wrong one.
Combined optional helpers
Named helpers pair the common primitive parsers with optional:
Parse.intOption (Some "42") // Ok (Some 42)
Parse.boolOption None // Ok None
Parse.decimalOption (Some "12.5") // Ok (Some 12.5M)
Parse.guidOption (Some "89d45a4b-f634-4db0-9a41-7e8461957be1")
// Ok (Some 89d45a4b-f634-4db0-9a41-7e8461957be1)
Reified.ParseModulePrimitive parsers for untrusted serialized input.
intOption: string option -> Result<int option,ParseError>Parses an optional integer. Absence returns Ok None; malformed present text returns its parsing error. Parse.intOption (Some "42") = Ok (Some 42)
SomeThe representation of "Value of type 'T" The input value. An option representing the value.
boolOption: string option -> Result<bool option,ParseError>Parses an optional Boolean. Absence returns Ok None; malformed present text returns its parsing error. Parse.boolOption (Some "true") = Ok (Some true)
NoneThe representation of "No value"
decimalOption: string option -> Result<decimal option,ParseError>Parses an optional decimal. Absence returns Ok None; malformed present text returns its parsing error. Parse.decimalOption (Some "12.5") = Ok (Some 12.5M)
guidOption: string option -> Result<System.Guid option,ParseError>Parses an optional GUID. Absence returns Ok None; malformed present text returns its parsing error. Parse.guidOption None = Ok None
The defaulting helpers pair them with optionalOr:
Parse.intOrDefault 80 None // Ok 80
Parse.boolOrDefault false (Some "true") // Ok true
Parse.decimalOrDefault 5.5M (Some "bad") // Error (InvalidFormat ("decimal", "bad"))
Reified.ParseModulePrimitive parsers for untrusted serialized input.
intOrDefault: int -> string option -> Result<int,ParseError>Parses an optional integer, using the supplied fallback only when the input is absent. Parse.intOrDefault 80 None = Ok 80
NoneThe representation of "No value"
boolOrDefault: bool -> string option -> Result<bool,ParseError>Parses an optional Boolean, using the supplied fallback only when the input is absent. Parse.boolOrDefault false None = Ok false
SomeThe representation of "Value of type 'T" The input value. An option representing the value.
decimalOrDefault: decimal -> string option -> Result<decimal,ParseError>Parses an optional decimal, using the supplied fallback only when the input is absent. Parse.decimalOrDefault 5.5M None = Ok 5.5M
Use *Option when absence should stay None, and *OrDefault when absence should become a concrete value. Both
preserve the error from malformed present text.
Parse, then refine
Parsing changes representation. Checking a typed value is a constraint's job, and admitting it into a domain type is a refinement's. They stay separate steps, and each contributes its own error:
open Reified
open Reified.Refinements
open Reified.Result
open Reified.ResultDSL
type QuantityError =
| InvalidInteger of ParseError
| InvalidQuantity of Violation
let quantity raw =
result {
let! parsed = Parse.int raw |> Result.mapError InvalidInteger
let! quantity = parsed |> Constraint.guard (Constraint.greaterThan 0) |> Result.mapError InvalidQuantity
return quantity
}
ReifiedRefinementsResultReified.ResultDSLThe concise result vocabulary: the result { } computation expression, its accumulating result.list { } / result.array { } variants, and the lightweight admission functions (okIf, failIf, require, orError, mapError). Optional and opt-in, in the same shape as Reified.DataDSL, Reified.ConstraintDSL, and Reified.SchemaDSL: open Reified.Result for Result, then open Reified.ResultDSL for this vocabulary. Deliberately small: generic combinators such as map, bind, orElse, tap, and the traversal helpers stay qualified as Result.map, Result.bind, and so on.
05-parsing__index.md_page.QuantityErrorInvalidIntegerReified.ParseErrorPrimitive parse failures returned by Parse helpers.
InvalidQuantityReified.ViolationWhy a value failed its constraint. A diagnostic contract, not an application error union. Domain code maps a whole violation once with Result.mapError; Schema adds the path at which it occurred. Violations are plain comparable data. No closure and no constraint description is reachable from one, so structural equality holds and a violation can be retained and compared long after the constraint that produced it went out of scope. There is no promised wire format. Reified-produced groups are never empty and never unary: a single failing child is returned directly rather than wrapped. The first * rest shape encodes non-emptiness only; non-unarity is a normalization invariant.
quantity: string -> Result<int,QuantityError>raw: stringresult: ResultBuilderThe fail-fast result { } computation expression.
parsed: intReified.ParseModulePrimitive parsers for untrusted serialized input.
int: string -> Result<int,ParseError>Parses a 32-bit integer.
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Reified.Result.ResultModuleFail-fast helpers over the standard F# Result type.
mapError: ('a -> 'b) -> Result<'c,'a> -> Result<'c,'b>Maps the error value of a result.
quantity: intReified.ConstraintModuleCreates, executes, composes, and inspects constraints.
guard: Constraint<'value> -> 'value -> Result<'value,Violation>Runs a constraint and returns the unchanged value after success. let requiredName : Constraint<string> = Constraint.present "Alice" |> Constraint.guard requiredName |> Result.mapError Violation.render
greaterThan: 'value -> Constraint<'value>Requires a value strictly greater than the supplied bound. let quantity : Constraint<int> = Constraint.greaterThan 0
See Refined values for the refinement model, and the Parse API reference for every parser.