Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultReified gives F# applications one model for trusted values and structured boundaries on .NET and Fable. Declare an invariant once; checking, diagnostics, codecs, contract documents, and test data read the same declaration.
A compiled JSON codec runs under NativeAOT and trimming without runtime reflection. In the recorded benchmark,
it is competitive with System.Text.Json on serialization and faster on decode because it avoids reflection and
boxing on each call. A schema parses input and provides field-aware diagnostics, JSON Schema export, and
localizable messages. Refined types prove an invariant at construction, so later code need not re-check it.
Parse and Result handle typed conversion and composition; Data is a source-neutral structured value.
The example below is compiled and executed on every CI run from
examples/Reified.GettingStarted;
the outputs below are that program's real output.
How the packages fit
Constraint ─┐
Parse ──────┤
Data ───────┼──> Schema ───> contract tooling
Refinements ┘
Use Schema when structured input must become a trusted model. Constraint, Parse, Data, and Refinements can each be used alone without Schema, and work together. "Contract tooling" is the generated derivation, versioning, and MSBuild pipeline built on Schema, covered from Derived Schemas onward, not a separate package.
Install the complete runtime set for this walkthrough:
dotnet add package Reified
The model is an ordinary record with no Reified-specific members:
type Signup =
{ Email: string
Age: int
Newsletter: bool }
FsLiveDocsGeneratedPage1_10EA2B4D0E31.SignupEmail: stringstringAn abbreviation for the CLI type . Basic Types
Age: intintAn abbreviation for the CLI type . Basic Types
Newsletter: boolboolAn abbreviation for the CLI type . Basic Types
Declare how untrusted input becomes one:
open Reified
open Reified.SchemaDSL
open Reified.ConstraintDSL
let signupSchema =
schema<Signup> {
field _.Email { constraints [ present; email ] }
field _.Age { constrain (atLeast 13) }
field _.Newsletter
construct (fun email age newsletter ->
{ Email = email; Age = age; Newsletter = newsletter })
}
ReifiedReified.SchemaDSLThe concise schema-definition vocabulary: the record computation expression, its field and constructor forms, and the collection-schema operations. Optional and opt-in, in the same shape as Reified.DataDSL and Reified.ConstraintDSL: open Reified.Schema for Schema, then open Reified.SchemaDSL for this vocabulary. There is no constraint catalogue here. One Constraint vocabulary serves direct checking, refinement, and Schema, so a field block reaches for Constraint.email or an opened Reified.ConstraintDSL exactly as standalone code does. Boundary supply is Schema-owned and stays here as mustSupply and mayOmit.
Reified.ConstraintDSLConstraint constructors usable without the Constraint. prefix inside a module that declares value rules. Optional vocabulary, not another abstraction. Opening it makes a declaration read minLength 3 instead of Constraint.minLength 3; everything here is the same value the qualified name returns. Some constructors are deliberately left out because they shadow names the same validation code is likely to need in scope: contains, distinct, all, any, length, and between shadow core F# operations. Reach for those as Constraint.contains, Constraint.all, and so on, even inside a module that has opened this DSL. Constraint execution (Constraint.satisfies, Constraint.check, Constraint.guard) is likewise always qualified: ConstraintDSL declares constraints, Constraint.* executes and inspects them. orError and mapError are structural adapters matching the corresponding Result operations. They let a constraint pipeline retain its input and finish with the application's error type without adding an Reified.Result dependency. module SignupRules = open Reified.ConstraintDSL let age : Constraint<int> = atLeast 13 let contact : Constraint<string> = Constraint.all [ present; email ] let requireContact value = value |> Constraint.guard contact |> orError EmailRequired
signupSchema: Schema<Signup>schema: SchemaCeBuilder.SchemaBuilder<'model>Record-schema computation expression.
FsLiveDocsGeneratedPage1_10EA2B4D0E31.Signup``.ctor``: Quotations.Expr<(Signup -> string)> -> field<Signup,string>Declares a field, deriving its camel-cased wire name from the property getter. type Signup = { Email: string } field (fun (s: Signup) -> s.Email) // wire name "email"
``.ctor``: Quotations.Expr<('model -> 'target)> -> field<'model,'target>Declares a field, deriving its camel-cased wire name from the property getter. type Signup = { Email: string } field (fun (s: Signup) -> s.Email) // wire name "email"
_arg1: SignupEmail: stringConstraints: SchemaFieldSteps.FieldInitial<Signup,string> * Constraint<string> list -> SchemaFieldSteps.FieldConfigured<Signup,string>Adds portable constraints to the field's inferred schema in declaration order.
present: Constraint<^value>Alias for .
email: Constraint<string>Alias for .
``.ctor``: Quotations.Expr<(Signup -> int)> -> field<Signup,int>Declares a field, deriving its camel-cased wire name from the property getter. type Signup = { Email: string } field (fun (s: Signup) -> s.Email) // wire name "email"
_arg2: SignupAge: intConstrain: SchemaFieldSteps.FieldInitial<Signup,int> * Constraint<int> -> SchemaFieldSteps.FieldConfigured<Signup,int>Adds a portable constraint to the field's current schema value.
atLeast: 'a -> Constraint<'a>Alias for .
``.ctor``: Quotations.Expr<(Signup -> bool)> -> field<Signup,bool>Declares a field, deriving its camel-cased wire name from the property getter. type Signup = { Email: string } field (fun (s: Signup) -> s.Email) // wire name "email"
_arg3: SignupNewsletter: boolconstruct: 'constructor -> SchemaCeBuilder.ConstructorStep<'model,'constructor>Closes a record schema with a total constructor.
email: stringage: intnewsletter: boolRequires & types
Requires: open Reified.ConstraintDSL; open Reified.SchemaDSL (packages: Reified.Constraint, Reified.Schema)val signupSchema: Schema<Signup>Data is a source-neutral input tree, so the same schema reads a form post, CLI arguments, JSON, or
configuration. Each source adapts into Data before Schema.parse — the name-value pairs below use
Data.ofNameValues; JSON text becomes Data with Json.parseData (portable, .NET and Fable) or
Data.ofJsonDocument on .NET 8+:
let input =
Data.ofNameValues
[ "email", "ada@example.org"
"age", "36"
"newsletter", "true" ]
Schema.parse signupSchema input
// Ok { Email = "ada@example.org"; Age = 36; Newsletter = true }
input: DataReified.DataModuleofNameValues: (string * string) seq -> DataBuilds object-shaped data from name and value pairs.
Reified.SchemaConstruction, composition, parsing, and checking for universal schemas.
parse: Schema<'a> -> Data -> Result<'a,SchemaErrors>Parses source-neutral structured data, runs constraints and refinements, and invokes record constructors.
signupSchema: Schema<Signup>Requires & types
Requires: open Reified (packages: Reified.Data, Reified.Schema)val input: Data"36" arrived as text and landed as an int. Schema.parse returns a Signup only when every field and the
constructor succeeded; a failed parse returns SchemaErrors, never a Signup. Signup is an ordinary public
record, so callers can still construct one directly — the parse result is the value that validation has admitted.
For a malformed address, an age below the limit, and a missing field:
let invalidInput =
Data.ofNameValues
[ "email", "ada"
"age", "11" ]
match Schema.parse signupSchema invalidInput with
| Ok signup -> printfn "%A" signup
| Error errors ->
for issue in SchemaErrors.toList errors do
printfn "%s: %s" (SchemaPath.format issue.Path) (SchemaError.render issue.Error)
invalidInput: DataReified.DataModuleofNameValues: (string * string) seq -> DataBuilds object-shaped data from name and value pairs.
Reified.SchemaConstruction, composition, parsing, and checking for universal schemas.
parse: Schema<'a> -> Data -> Result<'a,SchemaErrors>Parses source-neutral structured data, runs constraints and refinements, and invokes record constructors.
signupSchema: Schema<Signup>OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
signup: Signupprintfn: Printf.TextWriterFormat<'T> -> 'TPrint to stdout using the given format, and add a newline. The formatter. The formatted result. See Printf.printfn (link: ) for examples.
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
errors: SchemaErrorsissue: SchemaIssueReified.SchemaErrorsModuleFunctions for inspecting and rendering accumulated schema failures.
toList: SchemaErrors -> SchemaIssue listReturns failures in deterministic path order.
Reified.SchemaPathModuleFunctions for building and formatting schema error paths.
format: SchemaPath -> stringFormats a path with dot-separated keys and bracketed indexes.
Path: SchemaPathThe location of the failure.
Reified.SchemaErrorModuleFunctions for lowering and rendering boundary schema failures.
render: SchemaError -> stringRenders a schema error as an English sentence. Constraint violations delegate to Violation.render, so there is one rendering catalogue. SchemaError.render (SchemaError.InvalidFormat "int") // "Expected int format."
Error: SchemaErrorThe schema failure.
Requires & types
Requires: open Reified (packages: Reified.Data, Reified.Schema)val invalidInput: Dataage: Expected a value at least 13, but was 11.
email: Expected an email address, but was ada.
newsletter: This value was omitted.
Every independent field is checked even when others fail, so one parse reports every field problem rather than
the first. SchemaErrors.toList returns the failures in deterministic path order, and nested collection items
carry index paths such as lines[0].amount. Each sentence is rendered from the rule that failed. Cross-field
invariants live in the constructor, which runs only after every field passed; a constructor failure is reported as
a single diagnostic rather than a per-field problem.
The same signupSchema also produces a JSON codec:
open Reified
let codec = Json.compile signupSchema // compile once, typically at startup
Json.serialize codec { Email = "ada@example.org"; Age = 36; Newsletter = true }
// {"email":"ada@example.org","age":36,"newsletter":true}
Reifiedcodec: JsonCodec<Signup>Reified.JsonFunctions for compiling and running JSON codecs over built model schemas.
compile: Schema<'model> -> JsonCodec<'model>Compiles a completed schema into a reusable JSON codec. Compile once per schema, typically at startup, and reuse the codec for every value. Constructor-last object schemas retain a typed record plan, including checked constructors. Constructor failures surface as during decoding. Thrown when is null. Thrown when is incomplete. open Reified.SchemaDSL open Reified.ConstraintDSL type Customer = { Name: string; Email: string } let customerSchema = schema<Customer> { field _.Name { constrain present } field _.Email { constraints [ present; email ] } construct (fun name email -> { Name = name; Email = email }) } let customer = { Name = "Ada"; Email = "ada@example.org" } let codec = Json.compile customerSchema let json = Json.serialize codec customer let roundTripped = Json.deserialize codec json
signupSchema: Schema<Signup>serialize: JsonCodec<'model> -> 'model -> stringSerializes a trusted model to a JSON string through a compiled codec. Thrown when is null.
Email: stringAge: intNewsletter: boolRequires & types
Requires: open Reified (packages: Reified.Schema)val codec: JsonCodec<Signup>There is no second description of the wire shape to keep in step, and no runtime reflection: the codec is compiled from the schema's typed field plan, so it works under NativeAOT, trimming, and Fable.
One rule, several readers
One rule, "an age is at least 13", usually ends up written four times: in the parser that reads the request, in the validator that guards the domain, in the form that shows the message, and in the test that builds a fixture. They start identical and drift. Reified makes the rule a value instead: a checker can execute it, a renderer can explain it, and interpreters can export it or sample it. The same value scales from one field up to a whole model:
- On one value, a
Constraintanswers "is this valid?" and derives the failure. → Constraint - On a type, a refinement builds the rule into every construction, so downstream code need not re-check it. → Refined values
- On a model, refined types are field types, and boundary-specific rules attach as field constraints. → Schema quickstart
- From the same declaration, JSON Schema, a codec, versioned contracts, metadata, and test data are all derived. → What a schema gives you
Failures are ordinary F# values
Failures use the standard F# Result type rather than exceptions or a framework result type. Schema.parse
returns
Result<'model, SchemaErrors>; a value check returns Result<'value, Violation>. Both errors are data you can
match on, group by path, translate, or serialize into a problem-details response.
Reified.Result adds the composition, result { } for fail-fast sequencing, accumulating
builders for collecting every error at once, over the standard Result type rather than replacing it.
The words this documentation uses
The documentation distinguishes three operations:
| Word | Operation | Example |
|---|---|---|
| parse | Change one representation into another. Text becomes a typed value. | Parse.int "42" |
| The same move over a whole tree of untrusted input, accumulating every failure with its path. | input \|> Schema.parse signupSchema |
|
| check | Test a value you already hold against a rule. The value does not change. | Constraint.check (atLeast 13) age |
| refine | Give a checked value a type that carries the rule from then on, so nothing downstream re-checks it. | ContactEmail.create raw |
"Validation" is the everyday word for all of this, and the documentation uses it that way when talking about the area as a whole. Use the three terms above when the distinction matters.
Continue from here
For structured boundary models, continue with Schema.
The focused packages are independently installable when you need only one capability. See Packages and platforms for the complete inventory and platform support.