Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultSchemaDSL
Open Reified.SchemaDSL to declare schemas:
open Reified
open Reified.SchemaDSL
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.
Requires & types
Requires: (packages: Reified.Schema)A record schema is one constructor-last computation expression:
type Signup =
{ Email: string
Age: int }
static member create email age = { Email = email; Age = age }
schema<Signup> {
field _.Email
field _.Age
construct Signup.create
}
FsLiveDocsGeneratedPage9_10EA2B4D0E31.SignupEmail: stringstringAn abbreviation for the CLI type . Basic Types
Age: intintAn abbreviation for the CLI type . Basic Types
create: string -> int -> Signupemail: stringage: intschema: 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"
_arg1: Signup``.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: Signupconstruct: 'constructor -> SchemaCeBuilder.ConstructorStep<'model,'constructor>Closes a record schema with a total constructor.
Requires & types
Requires: open Reified.SchemaDSL (packages: Reified.Schema)Fields without blocks
field takes the getter and nothing else. The getter fixes the field type, Schema resolves that type's canonical
schema, and the wire name is the property name, camelCased:
field _.Name // wire name "name"
field _.Age // wire name "age"
field _.Tags // wire name "tags"This works for built-in primitives and composites, built-in refined values, and application types that contribute a
static Schema member.
Naming a field explicitly
fieldAs sets the wire name when it is not the camelCased property name. Explicit names are never transformed:
fieldAs "email_address" _.Email
fieldAs "type" _.Numberfield derives its name by reading a quotation of the getter, once, while the schema value is built. That runs on
.NET and on the Fable targets with quotation support, so both forms are available almost everywhere, see
Compiler-Directed, AOT, and Fable for the version and target requirements. fieldAs is
the portable spelling for Fable's Rust and PHP targets, which have no quotation support.
Accepting old field names
Add input-only aliases when a boundary must temporarily accept an earlier spelling or casing:
open Reified
open Reified.SchemaDSL
type Profile = { DisplayName: string }
let profileSchema =
schema<Profile> {
fieldAs "displayName" _.DisplayName {
alias "DisplayName"
aliases [ "display_name"; "display-name" ]
}
construct (fun displayName -> { DisplayName = displayName })
}
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.
FsLiveDocsGeneratedPage9_10EA2B4D0E31.ProfileDisplayName: stringstringAn abbreviation for the CLI type . Basic Types
profileSchema: Schema<Profile>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)
_arg1: ProfileAlias: SchemaFieldSteps.FieldInitial<Profile,string> * string -> SchemaFieldSteps.FieldConfigured<Profile,string>Adds one exact, input-only alternate wire name for this field.
Aliases: SchemaFieldSteps.FieldInitial<Profile,string> * string list -> SchemaFieldSteps.FieldConfigured<Profile,string>Adds exact, input-only alternate wire names for this field.
construct: 'constructor -> SchemaCeBuilder.ConstructorStep<'model,'constructor>Closes a record schema with a total constructor.
displayName: stringRequires & types
Requires: open Reified.SchemaDSL (packages: Reified.Schema)val profileSchema: Schema<Profile>Schema.parse and compiled JSON codecs accept the canonical name or any alias. Encoding, JSON Schema, diagnostic paths
for missing fields, and other output interpreters continue to use the canonical displayName name. Names are exact and
case-sensitive; list each accepted case explicitly.
Supplying more than one accepted name for the same field is an error. The schema builder rejects these eagerly when
the schema value is built, before any interpreter runs: it collects every field's canonical name and aliases and
throws System.ArgumentException (invalidArg) when any name appears more than once. That covers an alias that
repeats its own field's canonical name and an alias that collides with another field name or alias. Use
Versioned Contracts instead when a rename represents a durable versioned wire
format rather than a short compatibility window.
Field blocks
A block groups transformations for one field:
field _.Email {
withSchema Schema.text
constrain present
refine
validate validateCompanyEmail
}A field block is a typed pipeline. It starts at whatever came off the wire and has to end at the type the getter returns:
Data field -> Schema<string> -> raw constraints -> Schema<ContactEmail> -> domain validation
Every operation either preserves the current type or changes it, and there is exactly one that changes it:
| Operation | Effect on the current type |
|---|---|
withSchema |
Sets the starting type. |
constrain, constraints |
Preserve it. |
describe, format, defaultValue |
Preserve it. |
refine |
Changes it, from the raw type to the getter's type. |
validate |
Preserves it. |
Operations run from top to bottom:
withSchemasets the current raw schema. Without it, the field's type resolves the schema.constrainadds one constraint;constraintsadds a list in declaration order. Both preserve the value type.describe,format, anddefaultValueadd type-preserving metadata. They can use the inferred schema or followwithSchema;defaultValuealso supplies the field when input omits it.refinechanges the current schema from its raw type to the getter type.validateruns executable value-preserving logic over the current type.
Raw constraints have to come before refine, because after it the current type is ContactEmail and maxLength is
not a rule about a ContactEmail, it is a rule about the text that was admitted. Put text rules above the line and
domain rules below it.
The getter fixes where the pipeline must end. field _.Email on a ContactEmail member means the block has to arrive
at Schema<ContactEmail>, so a block that starts at Schema.text and never refines will not compile.
A plain int field needs no refinement, because it starts and ends at the same type:
open Reified.ConstraintDSL
type WithAge = { Age: int }
schema<WithAge> {
field _.Age {
withSchema Schema.int
constrain (atLeast 18)
}
construct (fun age -> { Age = age })
}
ReifiedReified.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
FsLiveDocsGeneratedPage9_10EA2B4D0E31.WithAgeAge: intintAn abbreviation for the CLI type . Basic Types
schema: SchemaCeBuilder.SchemaBuilder<'model>Record-schema computation expression.
``.ctor``: Quotations.Expr<(WithAge -> int)> -> field<WithAge,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"
``.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: WithAgeWithSchema: SchemaFieldSteps.FieldInitial<WithAge,int> * Schema<'current> -> SchemaFieldSteps.FieldWorking<WithAge,int,'current>Supplies the schema transformed by the remaining operations in this field block.
Reified.SchemaConstruction, composition, parsing, and checking for universal schemas.
int: Schema<int>Describes a 32-bit integer.
Constrain: SchemaFieldSteps.FieldInitial<WithAge,int> * Constraint<int> -> SchemaFieldSteps.FieldConfigured<WithAge,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.
age: intRequires & types
Requires: open Reified; open Reified.ConstraintDSL; open Reified.SchemaDSL (packages: Reified.Constraint, Reified.Schema)Group adjacent rules with constraints:
open Reified.ConstraintDSL
type WithGroupedEmail = { Email: string }
schema<WithGroupedEmail> {
field _.Email {
constraints [ present; email; maxLength 254 ]
}
construct (fun email -> { Email = email })
}
ReifiedReified.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
FsLiveDocsGeneratedPage9_10EA2B4D0E31.WithGroupedEmailEmail: stringstringAn abbreviation for the CLI type . Basic Types
schema: SchemaCeBuilder.SchemaBuilder<'model>Record-schema computation expression.
``.ctor``: Quotations.Expr<(WithGroupedEmail -> string)> -> field<WithGroupedEmail,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: WithGroupedEmailConstraints: SchemaFieldSteps.FieldInitial<WithGroupedEmail,string> * Constraint<string> list -> SchemaFieldSteps.FieldConfigured<WithGroupedEmail,string>Adds portable constraints to the field's inferred schema in declaration order.
present: Constraint<^value>Alias for .
email: Constraint<string>Alias for .
maxLength: int -> Constraint<^value>Alias for .
construct: 'constructor -> SchemaCeBuilder.ConstructorStep<'model,'constructor>Closes a record schema with a total constructor.
email: stringRequires & types
Requires: open Reified.ConstraintDSL; open Reified.SchemaDSL (packages: Reified.Constraint, Reified.Schema)The typed vocabulary in Reified.SchemaDSL covers every interpreted
constraint, the built-ins Reified can read as data and lower to JSON Schema. The field type checks every
entry, so email cannot be applied to an int field. Lifted constraints such as minLength apply to strings,
lists, arrays, and maps with shape-appropriate interpretation.
Constraint equivalents
The handwritten operations emitted for derivation attributes are listed beside the Schema API
reference. See Derivation Attributes
for the complete attribute mapping.
Refinement changes the type
field _.Email {
withSchema Schema.text
constrain present // operates on string
refine // string -> ContactEmail
validate validateCompanyEmail // operates on ContactEmail
}The parameterless operation resolves Refinement<string,ContactEmail> at compile time. A missing contribution is a
compile error; Schema does not use reflection or a runtime registry.
Refined value schemas works this through end to end, including writing the refinement itself.
Constructors
construct accepts a total constructor:
type WithEmailAge = { Email: string; Age: int }
schema<WithEmailAge> {
field _.Email
field _.Age
construct (fun email age -> { Email = email; Age = age })
}
FsLiveDocsGeneratedPage9_10EA2B4D0E31.WithEmailAgeEmail: stringstringAn abbreviation for the CLI type . Basic Types
Age: intintAn abbreviation for the CLI type . Basic Types
schema: SchemaCeBuilder.SchemaBuilder<'model>Record-schema computation expression.
``.ctor``: Quotations.Expr<(WithEmailAge -> string)> -> field<WithEmailAge,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"
_arg1: WithEmailAge``.ctor``: Quotations.Expr<(WithEmailAge -> int)> -> field<WithEmailAge,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: WithEmailAgeconstruct: 'constructor -> SchemaCeBuilder.ConstructorStep<'model,'constructor>Closes a record schema with a total constructor.
email: stringage: intRequires & types
Requires: open Reified.SchemaDSL (packages: Reified.Schema)constructResult accepts cross-field construction that can fail:
type CheckedSignup = { Email: string; Age: int }
module CheckedSignup =
let createChecked email age =
if age >= 13 then Ok { Email = email; Age = age }
else Error "Age must be at least 13."
schema<CheckedSignup> {
field _.Email
field _.Age
constructResult CheckedSignup.createChecked
}
FsLiveDocsGeneratedPage9_10EA2B4D0E31.CheckedSignupEmail: stringstringAn abbreviation for the CLI type . Basic Types
Age: intintAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage9_10EA2B4D0E31.CheckedSignupModulecreateChecked: string -> int -> Result<CheckedSignup,string>email: stringage: int(>=): 'T -> 'T -> boolStructural greater-than-or-equal The first parameter. The second parameter. The result of the comparison. 5 >= 1 // Evaluates to true 5 >= 5 // Evaluates to true [1; 5] >= [1; 6] // Evaluates to false
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.
schema: SchemaCeBuilder.SchemaBuilder<'model>Record-schema computation expression.
``.ctor``: Quotations.Expr<(CheckedSignup -> string)> -> field<CheckedSignup,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"
_arg1: CheckedSignup``.ctor``: Quotations.Expr<(CheckedSignup -> int)> -> field<CheckedSignup,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: CheckedSignupconstructResult: 'constructor -> SchemaCeBuilder.CheckedConstructorStep<'model,'constructor>Closes a record schema with a checked constructor.
Requires & types
Requires: open Reified.SchemaDSL (packages: Reified.Schema)val createChecked: string -> int -> Result<CheckedSignup,string>All independent fields must succeed before either constructor runs. A constructResult failure attaches to the current
object path.
The field chain is recursive and has no fixed arity limit.
Recursive schemas
Use Schema.defer where a field refers back to the schema being defined:
type Category =
{ Name: string
Children: Category list }
static member create name children = { Name = name; Children = children }
let rec categorySchema () =
schema<Category> {
field _.Name
field _.Children {
withSchema (Schema.listWith (Schema.defer categorySchema))
}
construct Category.create
}
FsLiveDocsGeneratedPage9_10EA2B4D0E31.CategoryName: stringstringAn abbreviation for the CLI type . Basic Types
Children: Category listlistThe type of immutable singly-linked lists. See the module for further operations related to lists. Use the constructors [] and :: (infix) to create values of this type, or the notation [1; 2; 3]. Use the values in the List module to manipulate values of this type, or pattern match against the values directly. See also F# Language Guide - Lists.
create: string -> Category list -> Categoryname: stringchildren: Category listcategorySchema: unit -> Schema<Category>schema: SchemaCeBuilder.SchemaBuilder<'model>Record-schema computation expression.
``.ctor``: Quotations.Expr<(Category -> string)> -> field<Category,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"
_arg1: Category``.ctor``: Quotations.Expr<(Category -> Category list)> -> field<Category,Category list>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"
_arg2: CategoryWithSchema: SchemaFieldSteps.FieldInitial<Category,Category list> * Schema<'current> -> SchemaFieldSteps.FieldWorking<Category,Category list,'current>Supplies the schema transformed by the remaining operations in this field block.
Reified.SchemaConstruction, composition, parsing, and checking for universal schemas.
listWith: Schema<'a> -> Schema<'a list>Describes a list using an explicit item schema.
defer: (unit -> Schema<'a>) -> Schema<'a>Defers a recursive schema reference until an interpreter needs it.
construct: 'constructor -> SchemaCeBuilder.ConstructorStep<'model,'constructor>Closes a record schema with a total constructor.
Requires & types
Requires: open Reified; open Reified.SchemaDSL (packages: Reified.Schema)val categorySchema: unit -> Schema<Category>Schema.defer takes a thunk, unit -> Schema<'model>, not a Lazy value: categorySchema
is declared as a zero-argument function specifically so Schema.defer categorySchema can pass it directly. The thunk
is memoized with F#'s thread-safe lazy, so a successful evaluation runs at most once and concurrent forces share
one result. A failed evaluation is not cached: the exception propagates to the caller and the next access retries the
thunk.