Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultRedisplay And Field Errors
When boundary input fails to parse, a form should show the user's original text next to each field's errors. Reified's
RetainedParseResult keeps both: the structured data exactly as submitted, and diagnostics addressed by path.
The Handoff Value
Use Schema.parseRetainingInput when the boundary needs the submitted representation after parsing:
open Reified
open Reified.SchemaDSL
open Reified.ConstraintDSL
type Customer = { Email: string; Age: int }
let customerSchema =
schema<Customer> {
field _.Email { constraints [ present; email ] }
field _.Age { constrain (atLeast 13) }
construct (fun email age -> { Email = email; Age = age })
}
let raw = Data.ofNameValues [ "email", "not-an-email"; "age", "12" ]
let parsed = Schema.parseRetainingInput customerSchema raw
parsed.IsValid // true when a trusted model exists
parsed.Result // Ok model | Error diagnostics
parsed.Input // the original Data, always retained
parsed.Errors // flattened path-aware errors ([] when valid)
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
FsLiveDocsGeneratedPage14_76B7E1D94D35.CustomerEmail: stringstringAn abbreviation for the CLI type . Basic Types
Age: intintAn abbreviation for the CLI type . Basic Types
customerSchema: Schema<Customer>schema: SchemaCeBuilder.SchemaBuilder<'model>Record-schema computation expression.
``.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"
``.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: CustomerConstraints: SchemaFieldSteps.FieldInitial<Customer,string> * Constraint<string> list -> SchemaFieldSteps.FieldConfigured<Customer,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<(Customer -> int)> -> field<Customer,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: CustomerConstrain: SchemaFieldSteps.FieldInitial<Customer,int> * Constraint<int> -> SchemaFieldSteps.FieldConfigured<Customer,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.
email: stringage: intraw: DataReified.DataModuleofNameValues: (string * string) seq -> DataBuilds object-shaped data from name and value pairs.
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.
IsValid: boolReturns true when input parsing produced a trusted model.
Result: Result<'value,SchemaErrors>The parsed model or accumulated schema failures.
Input: DataThe structured boundary data that was parsed.
Errors: SchemaIssue listReturns path-aware issues from a failed parse, or an empty list after a successful parse.
Schema parsing, schema validation, primitive Parse failures, Refine
failures, and value-level Violation values all lower to the same boundary taxonomy: SchemaError.
Field Error Lookup
ErrorsFor addresses errors with the same path text used by structured data, including collection indexes:
parsed.ErrorsFor "email" // errors attached exactly to the email field
parsed.ErrorsFor "contacts[1].value" // errors on the second contact's value, if this model had one
parsed: RetainedParseResult<Customer>ErrorsFor: string -> SchemaError listReturns schema errors attached exactly to the supplied structured data path.
SchemaError deliberately omits the field name, the diagnostics path already carries it, so the same error value
renders correctly wherever it is attached.
Redisplay
Data addresses submitted values by the same paths:
Data.redisplayPath "email" parsed.Input // "not-an-email", exactly as typed
Data.redisplayPath "contacts[1].value" parsed.Input
Reified.DataModuleredisplayPath: string -> Data -> stringParses a path and redisplays its scalar.
parsed: RetainedParseResult<Customer>Input: DataThe structured boundary data that was parsed.
Absent input looks up as Data.Null and redisplays as blank text, so form templates never special-case absent fields.
Rendering A Form
The typical loop over a failed parse:
type FormField = { Path: string }
let formFields = [ { Path = "email" }; { Path = "age" } ]
let render (field: FormField) (value: string) (errors: string list) =
printfn "%s = %A (%A)" field.Path value errors
for field in formFields do
let value = Data.redisplayPath field.Path parsed.Input
let errors = parsed.ErrorsFor field.Path |> List.map SchemaError.render
render field value errors
FsLiveDocsGeneratedPage14_76B7E1D94D35.FormFieldPath: stringstringAn abbreviation for the CLI type . Basic Types
formFields: FormField listrender: FormField -> string -> string list -> unitfield: FormFieldvalue: stringerrors: string 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.
printfn: 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.
Reified.DataModuleredisplayPath: string -> Data -> stringParses a path and redisplays its scalar.
parsed: RetainedParseResult<Customer>Input: DataThe structured boundary data that was parsed.
ErrorsFor: 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."
Because failed parses never construct the model, there is no half-valid object to guard against; the template works from structured data and diagnostics only.
For summary output, render every failed diagnostic in one line:
let messages = RetainedParseResult.renderErrors parsed
messages: string listReified.RetainedParseResultFunctions for creating and rendering retained schema parse results.
renderErrors: RetainedParseResult<'value> -> string listRenders one line for every failed schema issue.
parsed: RetainedParseResult<Customer>Localized Messages
SchemaError.render is the zero-dependency English default. To render in a language, pass a Renderer and let
Schema fold its typed path in as the attribute:
let signup = Renderer.english |> Renderer.context "signup"
match parsed.Result with
| Error errors ->
errors |> SchemaErrors.messages signup |> ignore
errors |> SchemaErrors.fullMessages signup |> ignore
errors |> SchemaErrors.toStringWith signup |> ignore // one full message per line
| Ok _ -> ()
signup: RendererReified.RendererModuleBuilds renderers and renders messages through them.
english: RendererA renderer that uses each catalogue's neutral English, with no resources at all. The default for tests, tools, and applications that never translate. It is not the same as Violation.render: this produces bare predicates that compose, while render keeps the legacy self-contained English exactly. violation |> Violation.fullMessage Renderer.english // "value must be present"
(|>): '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
context: string -> Renderer -> RendererAppends a document, model, form, or component segment. let signup = renderer |> Renderer.context "signup"
parsed: RetainedParseResult<Customer>Result: Result<'value,SchemaErrors>The parsed model or accumulated schema failures.
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
errors: SchemaErrorsReified.SchemaErrorsModuleFunctions for inspecting and rendering accumulated schema failures.
messages: Renderer -> SchemaErrors -> (SchemaPath * string) listRenders each failure as a localized predicate, paired with the path it occurred at. Predicates, not sentences: the returned SchemaPath already identifies the field, so a form that renders its own label would otherwise print the field name twice. Supply only the document context — Schema folds its typed path in as the attribute itself. errors |> SchemaErrors.messages (renderer |> Renderer.context "signup") // [ SchemaPath "name", "must be present" ]
ignore: 'T -> unitIgnore the passed value. This is often used to throw away results of a computation. The value to ignore. ignore 55555 // Evaluates to ()
fullMessages: Renderer -> SchemaErrors -> (SchemaPath * string) listRenders each failure as a complete fragment with its attribute noun, paired with its path. For API payloads and anywhere else without an adjacent label. At SchemaPath.root the noun comes from constraint.attribute.default; the document context is never used as a noun. errors |> SchemaErrors.fullMessages (renderer |> Renderer.context "signup") // [ SchemaPath "name", "Name must be present" ]
toStringWith: Renderer -> SchemaErrors -> stringRenders one localized line per failure. The localized counterpart of toString, using full messages so each line stands alone. errors |> SchemaErrors.toStringWith (renderer |> Renderer.context "signup")
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
messages returns bare predicates, which is what a form wants: the returned SchemaPath already identifies the field, so
a template that renders its own label does not print the name twice. fullMessages composes the attribute noun once
for payloads, logs, and summaries.
You supply only the document context. Index components stay out of resource keys, contacts[0].value and
contacts[1].value are one field for a translator, and stay in every returned path, so field lookup and redisplay
still work:
for field in formFields do
let value = Data.redisplayPath field.Path parsed.Input
let messages =
errors
|> SchemaErrors.messages signup
|> List.filter (fun (path, _) -> SchemaPath.format path = field.Path)
|> List.map snd
render field value messagesConstraint failures use Reified's constraint.* catalogue; Schema's own parse and structural failures have their own,
below. Both render through the same mechanics, so one renderer covers both. See
Localization for those mechanics and
Adding a language for generating a translation.
The Schema catalogue
Reified.Schema owns its own keys for parse, boundary-supply, and structural failures. They stay in that package;
Schema depends on Constraint and never the reverse. The key, argument, and default-English table is in the
SchemaMessages API reference.
SchemaMessages.keys, .arguments, and .english expose the same data. Constructor failures and custom errors
carrying authored prose have no catalogue entry: Schema does not invent a key for text your application wrote.
At SchemaPath.root, full rendering uses constraint.attribute.default, "value" in English, never the document context.
Mapping To Domain Errors
RetainedParseResult.Result stays typed to SchemaErrors, so translate interpreter errors into a domain or application
error type by mapping that Result while keeping the retained Input alongside it:
let domainResult =
parsed.Result
|> Result.mapError (fun errors ->
errors
|> SchemaErrors.toList
|> List.map (fun issue ->
SignupError.Field(SchemaPath.format issue.Path, SchemaError.render issue.Error)))
let submitted = parsed.InputWalking SchemaErrors.toList keeps each SchemaPath available while you build your own case, and parsed.Input still
redisplays the submitted values. That mapping is the boundary between Reified's interpreter errors and your application
errors. Keep your user-owned error union in the application, and translate SchemaError with one function when the parsed
result crosses that boundary.