Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultA model needs input parsing, validation of existing values, serialization, a published shape, UI metadata, older-payload support, and test data. Separate configuration, attributes, serializer contracts, Swagger annotations, and fixture builders repeat its rules.
One schema supplies all seven:
open Reified
open Reified.SchemaDSL
open Reified.ConstraintDSL
type Signup =
{ Email: string
Age: int
Newsletter: bool }
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
FsLiveDocsGeneratedPage0_76B7E1D94D35.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"
``.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.
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: 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 .
``.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: bool| What you need | Read the schema with | You get |
|---|---|---|
| input turned into a model | Schema.parse |
the model, or SchemaErrors with paths |
| a value you already hold checked | Schema.check |
the same value, or SchemaErrors |
| JSON in and out | Json.compile |
a reflection-free codec |
| the shape published | JsonSchema.generate |
a JSON Schema document |
| the model described to a UI | Inspect.model |
field metadata, for forms and admin UIs |
| older payloads accepted | Contract.parse |
old wire input mapped to the current model |
| test data that obeys the rules | SchemaGen |
generated values that satisfy the declaration (see Testing schema guarantees) |
Turn untrusted input into a model
Schema.parse takes a Data tree from a form post, query string, JSON, or configuration. It converts each field,
checks each rule, and calls the constructor.
Every independent field is checked, so a failure carries all problems with their paths, rather than only the
first. No Signup exists unless it passed the checks.
→ Schema quickstart · Input sources
Check a value you already hold
A record literal in a test, a row mapped from a database, or a payload deserialized elsewhere is already a
Signup value. Its type does not establish whether the rules hold. Schema.check runs the same field rules
and the same constructor against a value that already exists:
let imported = { Email = "ada"; Age = 11; Newsletter = false }
match Schema.check signupSchema imported 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)
// age: Expected a value at least 13, but was 11.
// email: Expected an email address, but was ada.
imported: 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>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.
Schema.check uses the parsing rules, error shape, and paths on an existing value. For models that remain publicly
constructible, this is the admission check. To make invalid values unrepresentable, see
Trusted construction.
Read and write JSON
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}
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>serialize: JsonCodec<'model> -> 'model -> stringSerializes a trusted model to a JSON string through a compiled codec. Thrown when is null.
Email: stringAge: intNewsletter: boolThere is no second description of the wire shape to keep in step. The codec is built from the schema's typed
field plan rather than from runtime type inspection, so it works under NativeAOT, trimming, and Fable. Records with many fields rely on build settings the
Reified.Schema package applies for NativeAOT; see Compiler-Directed, AOT, and Fable.
compile does the work up front; serialize and deserialize are the hot path. The codec enforces JSON
structure and required fields but skips constraint metadata, so decoding failures are not SchemaErrors:
Json.deserialize raises JsonCodecException with a schema-relative path, and Json.tryDeserialize returns the
first structural failure as Result<_, string>. Parse untrusted input with Schema.parse when you need
path-aware accumulated diagnostics.
Publish the shape to other tools
JsonSchema.generate signupSchema
// {"type":"object",
// "properties":{"email":{"type":"string", …},
// "age":{"type":"integer","minimum":13},
// "newsletter":{"type":"boolean"}},
// "required":["email","age","newsletter"]}
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>"minimum": 13 comes from the declaration's atLeast 13, so the published document uses the rule the code
enforces. Clients generated from it use the same shape as the parser and codec.
Describe the model to a form or admin UI
Inspect.model signupSchema
// email -> [ "present"; "email" ]
// age -> [ "atLeast" ]
// newsletter -> [ ]
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
signupSchema: Schema<Signup>Finite metadata: field names, shapes, the constraints on each. Nothing is parsed and nothing is checked. Forms, admin UIs, and documentation generators use it to describe the model, choose an input control per field, or show the rule before submission.
Accept payloads from older versions
A schema describes the model you have now. A contract registers the versions you have shipped, with a migration from each one, and reads versioned input into the current model:
match Contract.parse configContract raw with
| Ok config -> ...
| Error (ContractError.VersionUnrecognized version) -> ...
| Error (ContractError.ParseFailed(version, diagnostics)) -> ...
| Error (ContractError.Migration failure) -> ...Old clients keep working without version-specific fields in the current model. Malformed payloads carry
ContractError.ParseFailed with path-aware SchemaErrors. Migration failures carry ContractError.Migration: a
hand-written migration returns MigrationError.MigrationFailed (a message, no paths), while a migrated value that
fails the current schema's recheck returns MigrationError.RevalidationFailed with the same path-aware
SchemaErrors as Schema.parse.
Build test data that obeys the rules
SchemaGen derives values that satisfy the declaration's rules; raise the minimum age and the generated fixtures
follow. When a rule has no sound generation — a pattern, a custom predicate, or any opaque constraint — generation
fails closed with UnsupportedConstraint, naming the field path and the rule, instead of emitting fixtures that
ignore it.
open Reified.Schema.Testing
let generated = SchemaGen.model signupSchema
// Result<FsCheck.FSharp.Gen<Signup>, SchemaGenerationError>The generator lives in Reified.Schema.Testing; install it with dotnet add package Reified.Schema.Testing
(0.11.0 or later) and see Testing schema guarantees.