Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result
open Reified.Schema.JsonRedisplay And Field Errors
This page shows how failed schema parses retain structured data, path-aware field errors, and default display strings.
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:
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)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 valueSchemaError 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.InputAbsent 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:
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 errorsBecause 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
// [ "email: Expected email format."; "age: Must satisfy atLeast 13; got 12." ]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:
open Reified
let signup = renderer |> Renderer.context "signup"
errors |> SchemaErrors.messages signup
// [ SchemaPath "email", "must be a valid email"
// SchemaPath "contacts[1].value", "must be present" ]
errors |> SchemaErrors.fullMessages signup
// [ SchemaPath "email", "Email must be a valid email"
// SchemaPath "contacts[1].value", "Value must be present" ]
errors |> SchemaErrors.toStringWith signup // one full message per linemessages 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.
| Key | Arguments | Default English |
|---|---|---|
schema.omitted |
— | must be supplied |
schema.blank |
— | must be present |
schema.expectedScalar |
— | must be a single value |
schema.expectedObject |
— | must be an object |
schema.expectedMany |
— | must be a collection |
schema.invalidFormat |
expected |
must be a valid |
schema.parseOutOfRange |
target |
must be within the range of |
schema.unknownTag |
choices |
must be one of |
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.mapErrors translates interpreter errors into a domain or application error type at the boundary while
preserving the structured data and paths:
let domainParsed = parsed |> RetainedParseResult.mapErrors SignupError.ofSchemaErrorThat 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.