Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultNested Models And Collections Tutorial
This tutorial parses an order that contains a nested address and a collection of line items. Address and Item
own their canonical schemas, so Order can infer both the nested field and the list item schema.
Declare The Schemas
open Reified
open Reified.SchemaDSL
open Reified.ConstraintDSL
type Address =
{ Street: string; City: string }
static member Schema(_: Address) : Schema<Address> =
schema<Address> {
field _.Street
field _.City
construct (fun street city -> { Street = street; City = city })
}
type Item =
{ Sku: string; Quantity: int }
static member Schema(_: Item) : Schema<Item> =
schema<Item> {
field _.Sku
field _.Quantity {
constrain (greaterThan 0)
}
construct (fun sku quantity -> { Sku = sku; Quantity = quantity })
}
type Order =
{ Address: Address
Items: Item list }
let orderSchema =
schema<Order> {
field _.Address
field _.Items {
constrain (minLength 1)
}
construct (fun address items -> { Address = address; Items = items })
}
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
FsLiveDocsGeneratedPage4_10EA2B4D0E31.AddressStreet: stringstringAn abbreviation for the CLI type . Basic Types
City: stringSchema: Address -> Schema<Address>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.
schema: SchemaCeBuilder.SchemaBuilder<'model>Record-schema computation expression.
``.ctor``: Quotations.Expr<(Address -> string)> -> field<Address,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"
_arg2: Address_arg3: Addressconstruct: 'constructor -> SchemaCeBuilder.ConstructorStep<'model,'constructor>Closes a record schema with a total constructor.
street: stringcity: stringFsLiveDocsGeneratedPage4_10EA2B4D0E31.ItemSku: stringQuantity: intintAn abbreviation for the CLI type . Basic Types
Schema: Item -> Schema<Item>``.ctor``: Quotations.Expr<(Item -> string)> -> field<Item,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"
_arg2: Item``.ctor``: Quotations.Expr<(Item -> int)> -> field<Item,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"
_arg3: ItemConstrain: SchemaFieldSteps.FieldInitial<Item,int> * Constraint<int> -> SchemaFieldSteps.FieldConfigured<Item,int>Adds a portable constraint to the field's current schema value.
greaterThan: 'a -> Constraint<'a>Alias for .
sku: stringquantity: intFsLiveDocsGeneratedPage4_10EA2B4D0E31.OrderAddress: AddressItems: Item 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.
orderSchema: Schema<Order>``.ctor``: Quotations.Expr<(Order -> Address)> -> field<Order,Address>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: Order``.ctor``: Quotations.Expr<(Order -> Item list)> -> field<Order,Item 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"
_arg2: OrderConstrain: SchemaFieldSteps.FieldInitial<Order,Item list> * Constraint<Item list> -> SchemaFieldSteps.FieldConfigured<Order,Item list>Adds a portable constraint to the field's current schema value.
minLength: int -> Constraint<^value>Alias for .
address: Addressitems: Item listRequires & types
Requires: open Reified; open Reified.ConstraintDSL; open Reified.SchemaDSL (packages: Reified.Constraint, Reified.Schema)val orderSchema: Schema<Order>Adapt Nested Input
Configuration-style keys carry nesting with : separators and numeric collection indexes; JSON-like input produces
the same Data shape:
let raw =
Data.ofConfiguration
[ "address:street", "12 Analytical Way"
"address:city", "London"
"items:0:sku", "SKU-1"
"items:0:quantity", "2"
"items:1:sku", "SKU-2"
"items:1:quantity", "0" ]
raw: DataReified.DataModuleofConfiguration: (string * string) seq -> DataBuilds structured data from flattened configuration keys.
Requires & types
Requires: open Reified (packages: Reified.Data)val raw: DataParse And Read Item Errors
Every item is parsed and every item error accumulates into the same SchemaErrors; one bad line item does not hide
the others:
let parsed = Schema.parseRetainingInput orderSchema raw
parsed.ErrorsFor "items[1].quantity" // quantity 0 fails greaterThan 0
parsed.ErrorsFor "items[0].sku" // [], the first item is fine
parsed: RetainedParseResult<Order>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.
orderSchema: Schema<Order>raw: DataErrorsFor: string -> SchemaError listReturns schema errors attached exactly to the supplied structured data path.
Requires & types
Requires: open Reified (packages: Reified.Schema)val parsed: RetainedParseResult<Order>Nested diagnostics are prefixed with the field name (address.city), collection diagnostics with the item index
(items[1].quantity), and the raw values redisplay by the same paths:
Data.redisplayPath "items[1].quantity" parsed.Input // "0"
Reified.DataModuleredisplayPath: string -> Data -> stringParses a path and redisplays its scalar.
parsed: RetainedParseResult<Order>Input: DataThe structured boundary data that was parsed.
Requires & types
Requires: open Reified (packages: Reified.Data, Reified.Schema)Count Constraints
Collection constraints (minLength, maxLength, lengthBetween, distinct) attach to the collection field itself
and report on the collection path (items), separately from per-item errors.
When A Nested Field Needs More Than Its Canonical Schema
Address and Item above resolve through their own static member Schema, which is why Order's fields need no
withSchema. Reach for a field block with withSchema instead when a nested type has no canonical schema of its
own, Input Sources builds one locally for exactly that case, or when the field is
recursive, SchemaDSL's recursive schemas works a Schema.defer example
through in full.
Next
- Input Sources for the full adapter catalog.
- SchemaDSL for
withSchemaand recursive composition withSchema.defer.