Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result

Schema Integration

Schema describes structured input: fields, wire representations, path-aware failures, accumulation, and reconstruction. A refinement supplies the value-level step that turns an already-decoded underlying value into an invariant-carrying domain value. Schema can apply that refinement at a field boundary and report its failures at the field's path.

Reified.Refinements has no Schema dependency. Domain types can define refinements without choosing a wire format; an application that uses Reified.Schema decides where those refinements participate in structured decoding and encoding.

The Schema quickstart introduces fields, record construction, and path-aware diagnostics. Then read Refined Values in Schema for canonical field schemas, raw-value constraints, explicit refinement, and schema-local restrictions.

Refine a primitive schema

Verified
open Reified.Refinements
open Reified

let nameSchema : Schema<NonBlankString> =
    Schema.text
    |> Schema.refine NonBlankString.refinement
Requires & types
Requires: open Reified; open Reified.Refinements (packages: Reified.Refinements, Reified.Schema)
val nameSchema: Schema<NonBlankString>

Parsing checks the underlying string, constructs NonBlankString, and reports failures at the schema path. Encoding and checking project through NonBlankString.Value.

A numeric range is a constraint rather than a refined type, so it goes on the primitive:

Fragment
field _.Quantity { constrain (Constraint.greaterThan 0) }

Schema.constrain is available for a standalone value schema too, but inside a field block the schema is inferred from the field's type and each constraint sits on its own line.

For an application type:

Isolated
open Reified
open Reified.Refinements

type ContactEmail = private ContactEmail of string

module ContactEmail =
    let value (ContactEmail value) = value
    let refinement = Refinement.define Constraint.email ContactEmail value

let emailSchema : Schema<ContactEmail> =
    Schema.text
    |> Schema.refine ContactEmail.refinement

A field block receives the refinement explicitly:

Isolated
open Reified
open Reified.SchemaDSL
open Reified.Refinements

type ContactEmail = private ContactEmail of string

module ContactEmail =
    let value (ContactEmail value) = value
    let refinement = Refinement.define Constraint.email ContactEmail value

type Signup = { Email: ContactEmail; Age: int }

module Signup =
    let create email age = { Email = email; Age = age }

let signupSchema =
    schema<Signup> {
        field _.Email {
            withSchema Schema.text
            refine ContactEmail.refinement
        }

        field _.Age
        construct Signup.create
    }

Choose the operation by meaning

  • Schema.refine refinement schema constructs an invariant-carrying destination and retains refinement metadata.
  • Schema.convert forward backward schema performs a total projected mapping.
  • Schema.tryConvert forward backward schema performs a fallible projected mapping returning SchemaError list.
  • Schema.admit create project draftSchema constructs a domain model from a structured draft while preserving fields.
Verified
type Cents = Cents of int

let centsSchema : Schema<Cents> =
    Schema.int
    |> Schema.convert Cents (fun (Cents cents) -> cents)
Requires & types
Requires: open Reified (packages: Reified.Schema)
val centsSchema: Schema<Cents>

convert requires a total forward mapping, because it constructs the value with no error channel. A narrowing backward mapping such as decimal → int truncates fractional values and can throw on out-of-range input, so it belongs behind Schema.tryConvert with an explicit error, not in a total convert. The example above stays reversible in both directions.

A draft stays public and freely constructible; only the aggregate it admits into enforces the invariant, see Construction Guarantees for the full rationale. DateTimeOffset, not DateOnly, for the same reason Build A Private Aggregate gives: a canonical schema for every target. The draft's own schema is declared before the private Booking below it reuses the same field names, declaring it after would make _.Start resolve against Booking instead:

Isolated
open Reified
open Reified.SchemaDSL
open System

type BookingDraft = { Start: DateTimeOffset; End: DateTimeOffset }

let bookingDraftSchema : Schema<BookingDraft> =
    schema<BookingDraft> {
        field _.Start
        field _.End
        construct (fun start finish -> { Start = start; End = finish })
    }

type Booking =
    private
        { Start: DateTimeOffset
          End: DateTimeOffset }

module Booking =
    let create (draft: BookingDraft) : Result<Booking, SchemaError list> =
        if draft.Start <= draft.End then Ok { Start = draft.Start; End = draft.End }
        else Error [ SchemaError.Custom("end-before-start", Some "End must not be before start.") ]

    let toDraft (booking: Booking) : BookingDraft =
        { Start = booking.Start; End = booking.End }

let bookingSchema : Schema<Booking> =
    bookingDraftSchema
    |> Schema.admit Booking.create Booking.toDraft

For the complete progression from a raw field schema to a canonical refined field, continue to Refined Values in Schema.