Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultConstruction guarantees
An invariant is a rule that must hold for every valid value, such as a booking start date not following its end date. An aggregate is a group of related values created and updated as one thing.
Getting Started introduces refined fields, field constraints, and the private-model-plus-draft shape in order. This page is the reference behind those choices: what each level promises, where the promise stops, and how updates work at each one.
Choose the guarantee you need
There are four useful levels:
- A raw, wire, or draft value is editable and proves nothing.
- A successful
Schema.parseorSchema.checkresult passed that operation's gates. - A private refined field preserves one field-local invariant wherever the value is used.
- A private aggregate plus controlled transitions preserves relationships between fields.
Choose the simplest level that prevents a real application problem. Stronger representations add more construction and update functions, so use them where callers benefit from relying on the guarantee.
The limit
A library can guarantee the result of its own functions. It cannot prevent a caller from using another public constructor:
open Reified
open Reified.SchemaDSL
open System
type Booking =
{ Start: DateOnly
End: DateOnly }
let bookingSchema =
schema<Booking> {
field _.Start
field _.End
constructResult (fun start finish ->
if start <= finish then Ok { Start = start; End = finish }
else Error "Start must not be after end.")
}
let invalidAnyway =
{ Start = DateOnly(2026, 7, 20)
End = DateOnly(2026, 7, 12) }
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.
SystemFsLiveDocsGeneratedPage15_76B7E1D94D35.BookingStart: DateOnlySystem.DateOnlyRepresents dates with values ranging from January 1, 0001 Anno Domini (Common Era) through December 31, 9999 A.D. (C.E.) in the Gregorian calendar.
End: DateOnlybookingSchema: Schema<Booking>schema: SchemaCeBuilder.SchemaBuilder<'model>Record-schema computation expression.
``.ctor``: Quotations.Expr<(Booking -> DateOnly)> -> field<Booking,DateOnly>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: Booking_arg2: BookingconstructResult: 'constructor -> SchemaCeBuilder.CheckedConstructorStep<'model,'constructor>Closes a record schema with a checked constructor.
start: DateOnlyfinish: DateOnly(<=): 'T -> 'T -> boolStructural less-than-or-equal comparison The first parameter. The second parameter. The result of the comparison. 5 <= 1 // Evaluates to false 5 <= 5 // Evaluates to true [1; 5] <= [1; 6] // Evaluates to true
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
invalidAnyway: Booking``.ctor``: int * int * int -> unitCreates a new instance of the structure to the specified year, month, and day. The year (1 through 9999). The month (1 through 12). The day (1 through the number of days in ).
Schema.parse bookingSchema raw and Schema.check bookingSchema value enforce the constructor rule. The public record
literal does not. There is no honest library-level type guarantee for every Booking while that literal remains
available.
Durable field invariants
Private refined fields make illegal field values unrepresentable:
open Reified
open Reified.Refinements
type WorkspaceName = private WorkspaceName of string
module WorkspaceName =
let value (WorkspaceName name) = name
let refinement =
Refinement.define
(Constraint.all [ Constraint.present; Constraint.maxLength 80 ])
WorkspaceName
value
let create raw = Refinement.create refinement raw
let schema : Schema<WorkspaceName> =
Schema.text |> Schema.refine refinement
ReifiedRefinementsFsLiveDocsGeneratedPage15_76B7E1D94D35.WorkspaceNameWorkspaceNamestringAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage15_76B7E1D94D35.WorkspaceNameModulevalue: WorkspaceName -> string_arg1: WorkspaceNamename: stringrefinement: Refinement<string,WorkspaceName>Reified.Refinements.RefinementCreates and applies reusable refinement definitions.
define: Constraint<'underlying> -> ('underlying -> 'refined) -> ('refined -> 'underlying) -> Refinement<'underlying,'refined>Defines a refinement from one constraint, a constructor, and the reverse projection. Compose several rules with Constraint.all before defining, and reach for Constraint.custom when the rule is an arbitrary predicate. Both produce an ordinary constraint, so there is no separate plural or check-taking constructor. type RetryCount = RetryCount of int let retryCount = Refinement.define (Constraint.between 0 10) RetryCount (fun (RetryCount value) -> value)
Reified.ConstraintModuleCreates, executes, composes, and inspects constraints.
all: Constraint<'value> list -> Constraint<'value>Requires every constraint to hold, evaluating each in declaration order and accumulating failures. The empty list is the satisfied identity. F# visits list elements left to right, so annotate the binding when the first element is a type-directed value: let requiredName : Constraint<string> = Constraint.all [ Constraint.present; Constraint.lengthBetween 2 40 ]. let requiredName : Constraint<string> = Constraint.all [ Constraint.present; Constraint.lengthBetween 2 40 ]
present: Constraint<^value>Requires a value to be inhabited according to its shape. Whitespace-only text is blank, as are null text, a null or empty collection or map, None, ValueNone, and an empty Nullable. Blankness means .NET's whitespace set plus U+FEFF, which is what lets the rule be exported; see nonBlankPattern. The shape is selected from the return type, so a reusable binding needs its annotation: let requiredName : Constraint<string> = Constraint.present. Applied where the type is already known — inside an annotated rule, or to a schema — no annotation is needed. let requiredName : Constraint<string> = Constraint.present
maxLength: int -> Constraint<^value>Requires text or a collection to have at most the supplied size. let summary : Constraint<string> = Constraint.maxLength 280
create: string -> Result<WorkspaceName,Violation>raw: stringcreate: Refinement<'underlying,'refined> -> 'underlying -> Result<'refined,Violation>Constructs a refined value, reporting why the raw value was not admitted. type RetryCount = RetryCount of int let retryCount = Refinement.define (Constraint.between 0 10) RetryCount (fun (RetryCount value) -> value) 3 |> Refinement.create retryCount |> Result.mapError Violation.render
schema: Schema<WorkspaceName>Reified.Schema`1Describes a typed value's portable structure and construction for schema interpreters. A schema records shape and construction metadata without tying that metadata to input parsing, diagnostics, validation, codecs, UI generation, or workflow execution. Primitive, collection, optional, union, refined, and record declarations all produce Schema<'value>. Record declarations use the schema<'value> { } computation expression. Each field may contain withSchema, constrain, constraints, refine, and validate operations before the declaration finishes with construct or constructResult.
Reified.SchemaConstruction, composition, parsing, and checking for universal schemas.
text: Schema<string>Describes text input.
(|>): '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
refine: Refinement<'raw,'value> -> Schema<'raw> -> Schema<'value>Maps a raw schema through a reusable bidirectional refinement. The smart constructor runs during parsing. Inspection supplies the raw representation during checking and encoding.
Refinement.define keeps an executable check, readable metadata, total construction, and projection in one value.
Schema.refine applies that definition, so direct construction and Schema interpretation share the invariant.
Any WorkspaceName is valid because its representation is private and every exposed constructor returns Result.
The schema participates in that construction; it is not the sole guardian.
Durable aggregate invariants
Use a private aggregate representation when every value must satisfy a relationship between fields:
open Reified
open Reified.SchemaDSL
open System
type Booking =
private
{ Start: DateOnly
End: DateOnly }
module Booking =
let create start finish =
if start <= finish then Ok { Start = start; End = finish }
else Error "Start must not be after end."
let start booking = booking.Start
let finish booking = booking.End
let schema =
schema<Booking> {
fieldAs "start" start
fieldAs "end" finish
constructResult create
}
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.
SystemFsLiveDocsGeneratedPage16_76B7E1D94D35.BookingStart: DateOnlySystem.DateOnlyRepresents dates with values ranging from January 1, 0001 Anno Domini (Common Era) through December 31, 9999 A.D. (C.E.) in the Gregorian calendar.
End: DateOnlyFsLiveDocsGeneratedPage16_76B7E1D94D35.BookingModulecreate: DateOnly -> DateOnly -> Result<Booking,string>start: DateOnlyfinish: DateOnly(<=): 'T -> 'T -> boolStructural less-than-or-equal comparison The first parameter. The second parameter. The result of the comparison. 5 <= 1 // Evaluates to false 5 <= 5 // Evaluates to true [1; 5] <= [1; 6] // Evaluates to true
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
start: Booking -> DateOnlybooking: Bookingfinish: Booking -> DateOnlyschema: Schema<Booking>schema: SchemaCeBuilder.SchemaBuilder<'model>Record-schema computation expression.
fieldAs: string -> ('model -> 'value) -> field<'model,'value>Declares a field under an explicit wire name, for when the name differs from the property and for portable code. field _.Email derives the wire name from the property; reach for fieldAs when the wire name is not the camelCased property name, or when the code must also compile under Fable, which cannot run the quotation the derived form reads. Explicit names are never transformed. type Signup = { Email: string } fieldAs "email_address" (fun (s: Signup) -> s.Email)
constructResult: 'constructor -> SchemaCeBuilder.CheckedConstructorStep<'model,'constructor>Closes a record schema with a checked constructor.
The record schema invokes Booking.create, and ordinary callers cannot use a record literal. Updates must also go
through functions that preserve the rule. A private constructor with a public unchecked with-style escape hatch is
not complete encapsulation.
A private representation costs record syntax. Callers cannot write { Start = s; End = e } or { booking with End = e }
outside the defining module, and a positional create start finish loses the field names that make call sites readable.
The next section restores both without reopening the constructor.
Drafts
A draft is a public record whose only job is to be assembled and edited freely before admission. Give the private
aggregate a draft type, and make the schema's constructor the one path from draft fields to the domain type. Named
Reservation here, to keep this design distinct from the no-draft Booking above:
type ReservationDraft =
{ Start: DateOnly
End: DateOnly }
type Reservation =
private
{ Start: DateOnly
End: DateOnly }
module Reservation =
let create (draft: ReservationDraft) =
if draft.Start <= draft.End then Ok { Start = draft.Start; End = draft.End }
else Error "Start must not be after end."
let toDraft (reservation: Reservation) : ReservationDraft =
{ Start = reservation.Start; End = reservation.End }
let schema =
schema<Reservation> {
fieldAs "start" (fun r -> r.Start)
fieldAs "end" (fun r -> r.End)
constructResult (fun start finish -> create { Start = start; End = finish })
}
FsLiveDocsGeneratedPage15_76B7E1D94D35.ReservationDraftStart: DateOnlySystem.DateOnlyRepresents dates with values ranging from January 1, 0001 Anno Domini (Common Era) through December 31, 9999 A.D. (C.E.) in the Gregorian calendar.
End: DateOnlyFsLiveDocsGeneratedPage15_76B7E1D94D35.ReservationFsLiveDocsGeneratedPage15_76B7E1D94D35.ReservationModulecreate: ReservationDraft -> Result<Reservation,string>draft: ReservationDraft(<=): 'T -> 'T -> boolStructural less-than-or-equal comparison The first parameter. The second parameter. The result of the comparison. 5 <= 1 // Evaluates to false 5 <= 5 // Evaluates to true [1; 5] <= [1; 6] // Evaluates to true
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
toDraft: Reservation -> ReservationDraftreservation: Reservationschema: Schema<Reservation>schema: SchemaCeBuilder.SchemaBuilder<'model>Record-schema computation expression.
fieldAs: string -> ('model -> 'value) -> field<'model,'value>Declares a field under an explicit wire name, for when the name differs from the property and for portable code. field _.Email derives the wire name from the property; reach for fieldAs when the wire name is not the camelCased property name, or when the code must also compile under Fable, which cannot run the quotation the derived form reads. Explicit names are never transformed. type Signup = { Email: string } fieldAs "email_address" (fun (s: Signup) -> s.Email)
r: ReservationconstructResult: 'constructor -> SchemaCeBuilder.CheckedConstructorStep<'model,'constructor>Closes a record schema with a checked constructor.
start: DateOnlyfinish: DateOnlyConstruction keeps field names without exposing the representation:
let arrival = DateOnly(2026, 6, 1)
let departure = DateOnly(2026, 6, 5)
let reservation = Reservation.create { Start = arrival; End = departure }
arrival: DateOnly``.ctor``: int * int * int -> unitCreates a new instance of the structure to the specified year, month, and day. The year (1 through 9999). The month (1 through 12). The day (1 through the number of days in ).
departure: DateOnlyreservation: Result<Reservation,string>FsLiveDocsGeneratedPage15_76B7E1D94D35.ReservationModulecreate: ReservationDraft -> Result<Reservation,string>Start: DateOnlyEnd: DateOnlyThis is the same division the versioned contract chain uses. A wire record such as BookingV1 is also freely
constructible, but it serves transport and versioning; a draft serves local assembly and editing. The two roles can
share a type in small applications. Name the value draft, wire, or contract so the trust boundary stays visible,
and do not pass either shape through business logic as though its schema had changed the record's constructors.
The draft is not a hole in the guarantee. A ReservationDraft proves nothing and can hold any field values; only
Reservation.create and Schema.parse produce a Reservation, and both run the same rule. Code that skips the rule must
change the module that owns the representation, which is a visible, reviewable act rather than a quiet record literal
somewhere else in the codebase.
Updates
{ value with Field = x } is the shape F# programmers reach for, and each level of guarantee keeps a version of it.
Refined fields, public record. When every invariant is field-local, the record can stay public and with needs no
gate. The replacement value already went through its own constructor:
type Workspace = { Name: WorkspaceName }
match WorkspaceName.create "Ada's Workspace", WorkspaceName.create "New Name" with
| Ok initialName, Ok newName ->
let workspace = { Name = initialName }
let renamed = { workspace with Name = newName } // newName: WorkspaceName, proven at creation
ignore renamed
| _ -> ()
FsLiveDocsGeneratedPage15_76B7E1D94D35.WorkspaceName: WorkspaceNameFsLiveDocsGeneratedPage15_76B7E1D94D35.WorkspaceNameFsLiveDocsGeneratedPage15_76B7E1D94D35.WorkspaceNameModulecreate: string -> Result<WorkspaceName,Violation>OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
initialName: WorkspaceNamenewName: WorkspaceNameworkspace: Workspacerenamed: Workspaceignore: 'T -> unitIgnore the passed value. This is often used to throw away results of a computation. The value to ignore. ignore 55555 // Evaluates to ()
Refined field types leave record syntax, with, and structural equality untouched while making invalid field values
unrepresentable.
Schema-described public record. Update the record with ordinary with, then re-check it to run the schema's
constraints and record constructor against the changed value. With the public Booking from the first section:
let booking : Booking = { Start = DateOnly(2026, 6, 1); End = DateOnly(2026, 6, 5) }
let newEnd = DateOnly(2026, 6, 8)
let extended = { booking with End = newEnd }
match Schema.check bookingSchema extended with
| Ok checked -> printfn "%A" checked
| Error diagnostics -> printfn "%A" diagnostics
booking: BookingFsLiveDocsGeneratedPage15_76B7E1D94D35.BookingStart: DateOnly``.ctor``: int * int * int -> unitCreates a new instance of the structure to the specified year, month, and day. The year (1 through 9999). The month (1 through 12). The day (1 through the number of days in ).
End: DateOnlynewEnd: DateOnlyextended: BookingReified.SchemaConstruction, composition, parsing, and checking for universal schemas.
check: Schema<'a> -> 'a -> Result<'a,SchemaErrors>Checks an existing typed value, such as a freely constructed draft, through the schema's constraints, refinements, and record constructor.
bookingSchema: Schema<Booking>OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
checked: Bookingprintfn: 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.
diagnostics: SchemaErrorsThis suits models that remain publicly constructible, where Schema.check is the admission decision rather than the type.
Several fields, private aggregate. When fields must move together, shifting a booking changes both dates, lower to the draft, edit with ordinary record syntax, and re-admit:
let shift days reservation =
let draft = Reservation.toDraft reservation
Reservation.create { draft with Start = draft.Start.AddDays days; End = draft.End.AddDays days }
shift: int -> Reservation -> Result<Reservation,string>days: intreservation: Reservationdraft: ReservationDraftFsLiveDocsGeneratedPage15_76B7E1D94D35.ReservationModuletoDraft: Reservation -> ReservationDraftcreate: ReservationDraft -> Result<Reservation,string>FsLiveDocsGeneratedPage15_76B7E1D94D35.ReservationDraftStart: DateOnlyEnd: DateOnlyThe with expression works on the draft, so callers keep familiar syntax, and the result passes back through the one
authoritative constructor.
Every gated update returns Result: an edit can break the relationship, so an infallible with on the validated
type would bypass the check. When a
specific transition provably preserves the invariant, shifting both dates by the same amount cannot reorder them,
the owning module can expose it as a total function and keep the proof next to the representation.
Existing typed values
Schema.check is for values whose construction history is uncertain:
let importedBooking : Booking = { Start = DateOnly(2026, 6, 1); End = DateOnly(2026, 6, 5) }
match Schema.check bookingSchema importedBooking with
| Ok checked -> printfn "%A" checked
| Error diagnostics -> printfn "%A" diagnostics
importedBooking: BookingFsLiveDocsGeneratedPage15_76B7E1D94D35.BookingStart: DateOnly``.ctor``: int * int * int -> unitCreates a new instance of the structure to the specified year, month, and day. The year (1 through 9999). The month (1 through 12). The day (1 through the number of days in ).
End: DateOnlyReified.SchemaConstruction, composition, parsing, and checking for universal schemas.
check: Schema<'a> -> 'a -> Result<'a,SchemaErrors>Checks an existing typed value, such as a freely constructed draft, through the schema's constraints, refinements, and record constructor.
bookingSchema: Schema<Booking>OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
checked: Bookingprintfn: 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.
diagnostics: SchemaErrorsIt recursively checks fields and collections and re-invokes a record schema's constructor. It returns the original
value on success, not the constructor's result, so any normalization a construct or constructResult performs (such
as lowercasing an email) is discarded; the constructor runs only to verify cross-field invariants. Schema.check does
not create a proof wrapper, and the guarantee belongs to that successful result flow. A private domain representation
carries a stronger, durable guarantee.
Recommendation
The reference app uses all four levels deliberately:
WorkspaceV1andWorkspaceV2are public wire records.WorkspaceName,PersonName, andWorkItemTitlehave private representations and checked refinements.Workspacebusiness transitions accept only refined fields and returnResultfor relational business rules.- persisted contracts are parsed again when read; contextual production rules run only at the relevant admission boundary.
This division keeps schema metadata useful without claiming that metadata overrides F# construction semantics.
For complete project-sized examples, see Build A Private Aggregate,
Model Legal Transitions, and
Separate Wire And Domain Models. Those pages' Booking carries a draft type
and a typed BookingError, a fuller shape than this page's own no-draft Booking example above; both are the
same pattern at different levels of completeness, not two different designs.