Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultInput Sources
Data is the source-agnostic boundary shape: Null, Text, Number, Bool, List, and
Object. Adapters turn common sources into Data, and one schema parses them all. (Data is its own dependency-free
package, useful beyond schemas, see its docs.)
The Schema
open Reified
open Reified.SchemaDSL
open Reified.ConstraintDSL
type Address = { City: string }
type Contact = { Kind: string; Value: string }
type Customer =
{ Name: string
Address: Address
Contacts: Contact list }
let addressSchema =
schema<Address> {
field _.City
construct (fun city -> { City = city })
}
let contactSchema =
schema<Contact> {
field _.Kind
field _.Value
construct (fun kind value -> { Kind = kind; Value = value })
}
let customerSchema =
schema<Customer> {
field _.Name
field _.Address {
withSchema addressSchema
}
field _.Contacts {
withSchema (Schema.listWith contactSchema)
constrain (minLength 1)
}
construct (fun name address contacts ->
{ Name = name; Address = address; Contacts = contacts })
}
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
FsLiveDocsGeneratedPage9_76B7E1D94D35.AddressCity: stringstringAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage9_76B7E1D94D35.ContactKind: stringValue: stringFsLiveDocsGeneratedPage9_76B7E1D94D35.CustomerName: stringAddress: AddressContacts: Contact 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.
addressSchema: Schema<Address>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"
_arg1: Addressconstruct: 'constructor -> SchemaCeBuilder.ConstructorStep<'model,'constructor>Closes a record schema with a total constructor.
city: stringcontactSchema: Schema<Contact>``.ctor``: Quotations.Expr<(Contact -> string)> -> field<Contact,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: Contact_arg2: Contactkind: stringvalue: stringcustomerSchema: Schema<Customer>``.ctor``: Quotations.Expr<(Customer -> string)> -> field<Customer,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: Customer``.ctor``: Quotations.Expr<(Customer -> Address)> -> field<Customer,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"
``.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: CustomerWithSchema: SchemaFieldSteps.FieldInitial<Customer,Address> * Schema<'current> -> SchemaFieldSteps.FieldWorking<Customer,Address,'current>Supplies the schema transformed by the remaining operations in this field block.
``.ctor``: Quotations.Expr<(Customer -> Contact list)> -> field<Customer,Contact 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"
_arg3: CustomerWithSchema: SchemaFieldSteps.FieldInitial<Customer,Contact list> * Schema<'current> -> SchemaFieldSteps.FieldWorking<Customer,Contact 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.
Constrain: SchemaFieldSteps.FieldInitial<Customer,Contact list> * Constraint<Contact list> -> SchemaFieldSteps.FieldConfigured<Customer,Contact list>Adds a portable constraint to the field's current schema value.
minLength: int -> Constraint<^value>Alias for .
name: stringaddress: Addresscontacts: Contact listHere addressSchema and contactSchema are intentionally local value schemas. If Address and Contact declare
canonical intrinsic schemas, the Address line can use ordinary field, and the Contacts line can keep its field
block while omitting only withSchema: the list still resolves through Contact's canonical schema, but the block's
constrain (minLength 1) stays attached to the list field. That constraint constrains the length of the Contacts
list, so it belongs on the list field, not on Contact itself, which is a single record. A bare field _.Contacts
would drop the length check along with withSchema.
Nested fields expect object-shaped input and prefix their diagnostics with the field name; collection fields expect
Data.List, parse every item, accumulate every item error, and prefix diagnostics with the item index.
Each adapter below produces Data. The closing parse example uses the configuration input; the other examples
are isolated so their raw bindings do not collide.
HTTP Form-Like Input
Form posts and query strings are name/value pairs; repeated names become Data.List values:
open Reified
let raw =
Data.ofNameValues
[ "name", "Ada Lovelace"
"tag", "vip"
"tag", "beta" ] // repeated names accumulate into Data.List
Reifiedraw: DataReified.DataModuleofNameValues: (string * string) seq -> DataBuilds object-shaped data from name and value pairs.
Data.ofMap handles single-valued maps, and Data.ofNameValueCollection adapts
System.Collections.Specialized.NameValueCollection directly from ASP.NET-style APIs. Name/value sources are flat; use
the configuration or JSON adapters below when the input carries nested models or indexed collections.
CLI Arguments
open Reified
let raw = Data.ofCliArgs [ "--name"; "Ada Lovelace"; "--verbose"; "--no-color" ]
Reifiedraw: DataReified.DataModuleofCliArgs: string seq -> DataBuilds structured data from command-line arguments.
--name value, --name=value, -n value, boolean flags, --no-name, and repeated options are supported; positional
arguments collect under the _ field.
JSON Bodies With System.Text.Json
On .NET 8+ targets, adapt a parsed JsonDocument or JsonElement directly, the natural fit for ASP.NET Core
request bodies:
use! document = JsonDocument.ParseAsync request.Body
let raw = Data.ofJsonDocument documentJSON null, numbers, Booleans, arrays, and objects retain their corresponding Data cases. Number tokens keep their
exact lexical representation. The adapter uses the in-box System.Text.Json, so the package stays dependency-free.
Parse JSON portably
For the same lossless behavior on .NET and Fable, use Json.parseData from the installed Reified.Schema package:
open Reified
let raw = Json.parseData """{"name":"Ada","score":1.20e+3}"""
Reifiedraw: DataReified.JsonFunctions for compiling and running JSON codecs over built model schemas.
parseData: string -> DataParses one JSON value into source-neutral structured data. Preserves object field order, duplicate field names, and the original spelling of number tokens. This parser is available on .NET and Fable. It does not apply a model schema; use deserialize with a compiled codec when decoding directly to a schema-described model. Json.parseData "{\"name\":\"Ada\"}" // Data.Object [ "name", Data.Text "Ada" ] Thrown when is null. Thrown when the input is not one complete JSON value.
Under Fable, JSON.parse |> Data.ofJsonValue is also available when native JavaScript semantics are acceptable.
That route has already discarded duplicate fields and the original spelling of number tokens; use Json.parseData
when either distinction matters.
Configuration
Configuration keys use :-separated sections and numeric segments for collection indexes:
let raw =
Data.ofConfiguration
[ "name", "Ada Lovelace"
"address:city", "London"
"contacts:0:kind", "email"
"contacts:0:value", "ada@example.com" ]
raw: DataReified.DataModuleofConfiguration: (string * string) seq -> DataBuilds structured data from flattened configuration keys.
Later pairs override earlier ones at the same path, matching .NET configuration layering: a repeated key keeps its
last value, and a later scalar or section replaces the earlier shape at that key. A section path written after a
scalar at the same key replaces the scalar with the section, and a later scalar at a section key replaces the section;
the one exception is a section key whose value is null, as IConfiguration.AsEnumerable() emits alongside a section's
children — that never overrides the children, so real layered IConfiguration output round-trips directly. Collections
come from numeric segments, never from repetition; repeated names as multi-value input is a wire convention that
belongs to ofNameValues. Numeric segments need not be contiguous: a sparse set such as contacts:0 and contacts:2
becomes a three-item list with Data.Null at the missing index 1.
One Parse For All Of Them
Continuing with the configuration input above:
let parsed = Schema.parseRetainingInput customerSchema raw
match parsed.Result with
| Ok customer -> ignore customer
| Error diagnostics ->
// same paths for every source:
parsed.ErrorsFor "contacts[0].value" |> List.map SchemaError.render |> ignore
parsed: RetainedParseResult<Customer>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.
customerSchema: Schema<Customer>raw: DataResult: Result<'value,SchemaErrors>The parsed model or accumulated schema failures.
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
customer: Customerignore: 'T -> unitIgnore the passed value. This is often used to throw away results of a computation. The value to ignore. ignore 55555 // Evaluates to ()
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
diagnostics: SchemaErrorsErrorsFor: string -> SchemaError listReturns schema errors attached exactly to the supplied structured data path.
(|>): '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
Microsoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
map: ('T -> 'U) -> 'T list -> 'U listBuilds a new collection whose elements are the results of applying the given function to each of the elements of the collection. The function to transform elements from the input list. The input list. The list of transformed elements. let inputs = [ "a"; "bbb"; "cc" ] inputs |> List.map (fun x -> x.Length) Evaluates to [ 1; 3; 2 ]
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."
From C#
Consume-don't-author: F# declares the schema, C# parses and reads diagnostics. Most adapters take plain .NET types
already, ofNameValueCollection takes NameValueCollection, ofCliArgs and ofData take ordinary
sequences and values, and call as plain static methods. ofMap and ofConfiguration take F#-only types (Map and
a sequence of F# tuples), so use their C#-friendly equivalents instead:
using Reified.Schema;
// ofMap's C# equivalent, takes IDictionary<string, string> instead of an F# Map:
Data raw = DataModule.ofDictionary(new Dictionary<string, string> { ["name"] = "Ada Lovelace" });
// ofConfiguration's C# equivalent, takes the pairs IConfiguration.AsEnumerable() already returns:
Data fromConfig = DataModule.ofConfigurationPairs(configuration.AsEnumerable());
RetainedParseResult<Customer, SchemaError> parsed = Schema.parseRetainingInput(customerSchema, raw);
if (parsed.IsValid)
{
Customer customer = parsed.Value;
}
else
{
var errors = parsed.Errors; // FSharpList<Diagnostic<SchemaError>>
}
Schema.parseWith takes an F# function for its configure parameter, which C# cannot pass a lambda to directly. Use
Schema.parseWithOptions, which takes a Func<Options, Options> instead:
var parsed = Schema.parseWithOptions(o => Schema.constructorErrorAt("end", o), dateRangeSchema, raw);