Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultSignup Form Tutorial
This tutorial parses a signup form into a trusted model. If any field fails, no model is constructed and the form can be redisplayed with the user's original input and per-field errors.
Declare The Model And Schema
Declare the model, then attach a wire name, getter, and constraints to each field that needs one:
open Reified
open Reified.SchemaDSL
open Reified.ConstraintDSL
type Signup = { Email: string; Age: int }
let signupSchema =
schema<Signup> {
field _.Email {
constraints [ email; maxLength 254 ]
}
field _.Age {
constrain (atLeast 13)
}
construct (fun email age -> { Email = email; Age = age })
}
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
FsLiveDocsGeneratedPage3_76B7E1D94D35.SignupEmail: stringstringAn abbreviation for the CLI type . Basic Types
Age: intintAn abbreviation for the CLI type . Basic Types
signupSchema: Schema<Signup>schema: SchemaCeBuilder.SchemaBuilder<'model>Record-schema computation expression.
``.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: SignupConstraints: SchemaFieldSteps.FieldInitial<Signup,string> * Constraint<string> list -> SchemaFieldSteps.FieldConfigured<Signup,string>Adds portable constraints to the field's inferred schema in declaration order.
email: Constraint<string>Alias for .
maxLength: int -> Constraint<^value>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: SignupConstrain: 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 .
construct: 'constructor -> SchemaCeBuilder.ConstructorStep<'model,'constructor>Closes a record schema with a total constructor.
email: stringage: intschema<Signup> anchors the model type. The closing constructor must match every field in declaration order, so
missing or mistyped arguments fail at construct. Email stays a plain string here because its rule, a length
limit alongside the format check, is specific to this field rather than to every email in the codebase; see
Getting Started for when a rule belongs on a refined type instead.
Adapt The Structured Data
Form posts are name/value pairs:
let raw =
Data.ofNameValues
[ "email", "not-an-email"
"age", "12" ]
raw: DataReified.DataModuleofNameValues: (string * string) seq -> DataBuilds object-shaped data from name and value pairs.
Parse
let parsed = Schema.parseRetainingInput signupSchema raw
parsed: RetainedParseResult<Signup>Reified.SchemaConstruction, composition, parsing, and checking for universal schemas.
parseRetainingInput: Schema<'a> -> Data -> RetainedParseResult<'a>Parses source-neutral structured data while retaining it for redisplay and error lookup.
signupSchema: Schema<Signup>raw: Dataparsed is a RetainedParseResult<Signup>. On success parsed.Result is Ok signup and every constraint
already holds. Here both fields fail, so no Signup exists anywhere:
parsed.IsValid // false
parsed.ErrorsFor "email" // [ SchemaError.Violation ... ] for the email-format constraint
parsed.ErrorsFor "age" // [ SchemaError.Violation ... ] for the atLeast 13 constraint
parsed: RetainedParseResult<Signup>IsValid: boolReturns true when input parsing produced a trusted model.
ErrorsFor: string -> SchemaError listReturns schema errors attached exactly to the supplied structured data path.
Redisplay The Form
The original input is retained on the parsed value, addressed by the same paths:
Data.redisplayPath "email" parsed.Input // "not-an-email", exactly as typed
Data.redisplayPath "age" parsed.Input // "12"
Reified.DataModuleredisplayPath: string -> Data -> stringParses a path and redisplays its scalar.
parsed: RetainedParseResult<Signup>Input: DataThe structured boundary data that was parsed.
A form template needs only parsed.Input and parsed.ErrorsFor; there is no half-valid model to guard against.
Use SchemaError.render for field-level messages or RetainedParseResult.renderErrors parsed for a summary list.
Use The Trusted Model
match parsed.Result with
| Ok signup -> register signup // constraints already hold; no re-checking downstream
| Error _ -> renderForm parsedSignup here is a public record, so the guarantee belongs to the successful parse result, not to the type. Other
code can still write a Signup literal that skips the schema, which is fine for a boundary form model whose only
job is to be parsed once and used. When a value's construction history is uncertain instead, Schema.check signupSchema value re-runs the same constraints over an already assembled value.
Construction Guarantees covers the full division, including when an invariant
that must hold for every value of the type calls for a private representation rather than a public record.
Next
- Nested Models And Collections for models inside models.
- Redisplay And Field Errors for the full redisplay guide.
- Construction Guarantees for which claims need a private type rather than a schema.