Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultQuickstart
A Schema<'model> is one declaration of a structured boundary. It names fields, gives each a value schema, runs
constraints and refinements, accumulates failures with paths, and calls your constructor only once the fields succeed.
The same declaration drives several interpreters, so parsing, JSON serialization, JSON Schema generation, and metadata inspection stay in step with no second source of truth.
dotnet add package Reified.Schema
open Reified
open Reified.SchemaDSL
open Reified.ConstraintDSL
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
Requires & types
Requires: (packages: Reified.Constraint, Reified.Schema)First schema and parse
A schema can consist of primitives alone:
type Signup =
{ Email: string
Age: int
Newsletter: bool }
let signupSchema =
schema<Signup> {
field _.Email
field _.Age
field _.Newsletter
construct (fun email age newsletter ->
{ Email = email; Age = age; Newsletter = newsletter })
}
FsLiveDocsGeneratedPage2_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
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"
_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: Signup``.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: Signupconstruct: 'constructor -> SchemaCeBuilder.ConstructorStep<'model,'constructor>Closes a record schema with a total constructor.
email: stringage: intnewsletter: boolRequires & types
Requires: open Reified.SchemaDSL (packages: Reified.Schema)val signupSchema: Schema<Signup>field _.Email gives the getter and nothing more. The field's value schema comes from its type, string resolves
Schema.text, int resolves Schema.int, bool resolves Schema.bool, and the wire name is derived by lowercasing
only the property name's first character with Char.ToLowerInvariant, leaving the rest unchanged: Email becomes
email, but an acronym such as URLValue derives uRLValue, not urlValue. Use fieldAs when that default is not
the wire name you want, including for Unicode identifiers whose first character has no single-character lowercase
mapping. construct receives the fields in declaration order; the compiler checks its argument types and its
result.
Pass a wire name explicitly when it differs from the property: fieldAs "email_address" _.Email. To accept an old
input spelling while always writing the canonical name, use a field block with alias "Email" or
aliases [ "Email"; "emailAddress" ].
Deriving the name from _.Email uses a quotation. That compiles on .NET and on the Fable targets that support
quotations, including JavaScript; reach for fieldAs on Fable's Rust and PHP targets. See
SchemaDSL and Compiler-Directed, AOT, and Fable.
Parse
Data is a source-neutral input tree. The same schema reads form posts, CLI arguments, JSON, and configuration,
see Input Sources.
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" arrives as text and lands as int. Decoding a primitive from its serialized form is part of the field's value
schema.
No Signup is produced unless every field succeeds and the constructor succeeds.
Read the failures
Independent fields are all interpreted, so one parse reports every problem rather than the first:
match Schema.parse signupSchema input 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)
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>input: DataOkRepresents 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.Schema)Input missing newsletter and carrying "age": "not-a-number":
age: Expected int format.
newsletter: This value was omitted.
Paths come from the structure of the declaration. Application code never repeats field names, nested object names, list indexes, or map keys alongside separate validation expressions. For nesting and collections, see Nested Models And Collections.
Serialize
The Json module compiles the same declaration into a JSON codec. It uses no runtime reflection, so it works under
NativeAOT, trimming, and Fable.
let codec = Json.compile signupSchema
let json = Json.serialize codec { Email = "ada@example.org"; Age = 36; Newsletter = true }
// {"email":"ada@example.org","age":36,"newsletter":true}
Json.deserialize codec json
codec: 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>json: stringserialize: JsonCodec<'model> -> 'model -> stringSerializes a trusted model to a JSON string through a compiled codec. Thrown when is null.
Email: stringAge: intNewsletter: booldeserialize: JsonCodec<'model> -> string -> 'modelDeserializes a JSON string to a trusted model through a compiled codec. Thrown when or is null. Thrown when the JSON does not match the schema's wire shape.
Requires & types
Requires: open Reified (packages: Reified.Schema)val codec: JsonCodec<Signup>val json: stringCompile the codec once and reuse it. Json.compile does the work up front; serialize and deserialize are the hot
path. The compiled codec keeps no per-value state, so one codec is safe to share across threads: each
serialize/deserialize call uses its own pooled buffer and its own per-decode field slots. Compilation is a pure
walk over the built (immutable) schema, so it is also safe to run once at startup or concurrently with other
compilations. See JSON Codecs for buffers, streams, and decode diagnostics.
The other interpreters
The same signupSchema also drives:
JsonSchema.generate signupSchema
// {"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object",
// "properties":{"email":{"type":"string"},"age":{"type":"integer"},
// "newsletter":{"type":"boolean"}},
// "required":["email","age","newsletter"]}
Inspect.model signupSchema
// finite metadata: field names, shapes, constraints, no execution
Reified.JsonSchemaGenerates JSON Schema documents from built model schemas. The generator is a pure interpreter over descriptions: it lowers shapes, declared formats, and portable constraint metadata to JSON Schema keywords without parsing input, running checks, or constructing models. One schema declaration therefore drives parsing, validation, and the published contract. Lowering rules: primitives map to type (with format for dates, date-times, and uuids), refined values lower to their underlying primitive representation, nested models to object with properties and required, collections to array with items, maps to object with additionalProperties, and tagged unions to oneOf with a const-constrained discriminator property per case. An option lowers to its payload in an omittable record field, where the codec leaves None out, and to anyOf of its payload and null everywhere the codec writes None as null. Constraint metadata lowers to minLength, maxLength, pattern, enum, minimum/maximum (and exclusive variants), multipleOf, minItems/maxItems, and uniqueItems; constraints without a JSON Schema equivalent, such as trimmed, are skipped. Default-value metadata attached with Schema.withDefault lowers to default.
generate: Schema<'model> -> stringGenerates a compact JSON Schema document from any completed schema declaration. The record, primitive, collection, union, or other completed schema to lower. Thrown when is null. 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 document = JsonSchema.generate customerSchema // {"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{...},"required":[...]}
signupSchema: Schema<Signup>Reified.InspectThe inspection API over built schemas and value schemas. Inspect is the entry point for non-validation interpreters: JSON Schema generation, documentation, UI metadata, and codec planning all start from the same descriptions. The returned trees are plain immutable data — inspecting them never parses input, runs checks, or constructs models.
model: Schema<'model> -> ModelDescriptionDescribes a built model schema as inspectable field metadata. The built model schema to describe. Thrown when is null. Thrown when is not a completed model schema. let description = Inspect.model customerSchema let names = description.Fields |> List.map _.Name
Requires & types
Requires: open Reified (packages: Reified.Schema)| Input | Interpreter | Result |
|---|---|---|
Data |
Schema.parse |
model or SchemaErrors |
| an existing typed value | Schema.check |
the same value or SchemaErrors |
| schema | Json.compile |
reusable JSON codec |
| schema | JsonSchema.generate |
JSON Schema document |
| schema | Inspect.model |
metadata without execution |
versioned Data |
Contract.parse |
current model or ContractError |
Schema.check is for values that did not arrive as Data, a record literal, a database mapper's output, an import.
It runs the same field rules and calls the same constructor:
let existingValue = { Email = "ada@example.org"; Age = 36; Newsletter = true }
Schema.check signupSchema existingValue
existingValue: SignupEmail: stringAge: intNewsletter: boolReified.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.
signupSchema: Schema<Signup>Requires & types
Requires: open Reified (packages: Reified.Schema)val existingValue: SignupInspect.model is what forms, admin UIs, and documentation generators read.
Where to go next
Choose the least restrictive representation that prevents the problem:
- Plain fields, the schema is the admission decision.
- Refined fields, field-local invariants hold everywhere, and the record stays public with
withintact. - Field constraints, boundary-specific rules, visible where they apply.
- Private model and draft, relationships between fields.
- Refined fields in Schema, refined types as fields and how interpreters read them.
- Boundary-specific field rules, field blocks and boundary constraints.
- Construction Guarantees, what each rung does and does not promise.
- SchemaDSL, the full declaration vocabulary.
- Derived Schemas, generate that declaration from an attributed F# wire record.
- Field Blocks and Plain Functions, a field block read as ordinary functions over one
Schema. - Refined Schemas, your own domain types as fields.
- Tutorials, a signup form, nested models and collections, and metadata inspection.
- Redisplay And Field Errors, failed parses that keep the user's input.
- Versioned Contracts, evolving the wire format without freezing the domain model.