Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result
open Reified.Schema.JsonRefined Schemas
A field can be short when its type contributes a canonical schema:
let contactSchema =
schema<Contact> {
field _.Email
construct Contact.create
}This is the form to prefer at use sites. Every built-in refined type from
Reified.Refinements works this way — NonBlankString,
FiniteFloat, UnitInterval, NonEmptyList<_>, and the rest resolve without a withSchema, as
Getting Started shows.
Rules that need a parameter, such as a length range or a pattern, are constraints rather than types. They belong on the field, because the bounds are a property of this field rather than of the value:
field _.Name {
constrain Constraint.present
constrain (Constraint.lengthBetween 2 80)
}Expressing them this way means there is only ever one set of bounds — the schema's. An earlier BoundedString type
recorded the bounds it happened to be constructed under, so a value refined at 1..99 was still a BoundedString
when checked against a 2..80 schema, and the schema had to re-run its own bounds anyway.
Where a length or format rule should be part of a type, define your own refined type for it, as Lift universal constraints into the refinement shows. The test is whether any later operation relies on the rule; see When not to make a type.
The rest of this page expands what a domain type like Email contributes and shows where schema-local constraints fit.
Define the domain type
open Reified
open Reified.Refinements
type Email = private Email of string
module Email =
let value (Email value) = value
let refinement =
Refinement.define
Constraint.email
Email
value
let create value = Refinement.create refinement value
ReifiedRefinements08-schema_40-refined-values.md_page.EmailEmailstringAn abbreviation for the CLI type . Basic Types
08-schema_40-refined-values.md_page.EmailModulevalue: Email -> string_arg1: Emailvalue: stringrefinement: Refinement<string,Email>Reified.Refinements.RefinementCreates and applies reusable refinement definitions.
define: Constraint<'underlying> -> ('underlying -> 'refined) -> ('refined -> 'underlying) -> Refinement<'underlying,'refined>Defines a refinement from one constraint, a constructor, and the reverse projection. Compose several rules with Constraint.all before defining, and reach for Constraint.custom when the rule is an arbitrary predicate. Both produce an ordinary constraint, so there is no separate plural or check-taking constructor. type RetryCount = RetryCount of int let retryCount = Refinement.define (Constraint.between 0 10) RetryCount (fun (RetryCount value) -> value)
Reified.ConstraintModuleCreates, executes, composes, and inspects constraints.
email: Constraint<string>Requires text to match Reified's pragmatic email shape, ^[^@]+@[^@]+$. let contact : Constraint<string> = Constraint.email
create: string -> Result<Email,Violation>create: Refinement<'underlying,'refined> -> 'underlying -> Result<'refined,Violation>Constructs a refined value, reporting why the raw value was not admitted. type RetryCount = RetryCount of int let retryCount = Refinement.define (Constraint.between 0 10) RetryCount (fun (RetryCount value) -> value) 3 |> Refinement.create retryCount |> Result.mapError Violation.render
The email format is intrinsic to Email, so its refinement owns that constraint.
Apply constraints in the Schema DSL
Expand the field to expose its wire schema:
open Reified
open Reified.SchemaDSL
field _.Email {
withSchema Schema.text
constraints [ present; maxLength 80 ]
}Schema constraints remain available directly inside a field block. Here both constraints apply to the incoming
string, so interpreters can retain their metadata for diagnostics, forms, and generated schemas. The named Schema
constraints use the same executable metadata defined by Constraints.
Constraints preserve the value type, however. This block still contains a Schema<string>, while _.Email returns
Email; by itself, the declaration cannot complete the field.
Refine after constraining the raw value
Add refine after the raw-text constraints to perform that type transition:
let contactSchema =
schema<Contact> {
field _.Email {
withSchema Schema.text
constraints [ present; maxLength 80 ]
refine Email.refinement
}
construct Contact.create
}The operations run in declaration order:
withSchema Schema.textstarts withSchema<string>.constraintschecks facts that belong to the incoming text while preservingstring.refine Email.refinementconstructsEmail, producingSchema<Email>to match the getter.
A raw-text constraint must appear before refinement because it cannot be applied to the resulting Email value.
Keep one-off constraints at the schema boundary
Suppose only the billing form imposes an 80-character transport limit:
field _.BillingEmail {
withSchema Schema.text
constraints [ present; maxLength 80 ]
refine Email.refinement
}That limit belongs in this field block rather than in every Email. Email.refinement still enforces the intrinsic
email-format invariant for every construction path.
Use this inline form for boundary-specific restrictions. If a constraint must hold for every instance of the domain type, lift it into the refinement instead.
Attach an application constraint
There is no adapter step: Schema takes the same Constraint value you would check directly.
let even : Constraint<int> =
Constraint.custom "must be an even quantity" (fun value -> value % 2 = 0)
field _.Quantity {
constrain even
}An arbitrary predicate is opaque, so it runs during parsing and checking but is documented rather than enforced by
generated schemas. Composing built-ins instead — Constraint.multipleOf 2 here — keeps the rule inspectable, which is
what lets JSON Schema lower it and SchemaGen generate values that satisfy it. See
Interpreted and opaque for the trade.
Lift universal constraints into the refinement
If required presence and the length limit define every ContactEmail, put them beside the domain type instead:
type ContactEmail = private ContactEmail of string
module ContactEmail =
let value (ContactEmail value) = value
let refinement =
Refinement.defineAll
[ Reified.Constraint.Constraint.present
Reified.Constraint.Constraint.email
Reified.Constraint.Constraint.maxLength 254 ]
ContactEmail
value
let create value = Refinement.create refinement valueThe schema then carries the complete invariant through one value:
let contactEmailSchema : Schema<ContactEmail> =
Schema.text
|> Schema.refine ContactEmail.refinement
|> Schema.withFormat SchemaFormat.emailContribute that canonical schema once:
type ContactEmail with
static member Schema(_: ContactEmail) = contactEmailSchemaFields return to the compressed form:
schema<Contact> {
field _.Email
construct Contact.create
}The constraint-backed refinement now drives direct construction, Schema diagnostics, inspection metadata, and applicable wire interpreters.
Canonical refinement inference inside a field
A type may also contribute one canonical refinement for an underlying/destination pair:
type ContactEmail with
static member Refinement(_: string, _: ContactEmail) = ContactEmail.refinementThen an explicitly selected raw schema can use bare refine:
field _.Email {
withSchema Schema.text
refine
}Schema knows both string and ContactEmail at that declaration site. Use refine ContactEmail.refinement when a
local variant must be selected explicitly. Parsing and ordinary refinement construction remain named operations.
Choose the operation by meaning
Schema.refineconstructs an invariant-carrying destination and retains refinement metadata.Schema.convertperforms a total projected mapping.Schema.tryConvertperforms a fallible projected mapping returningSchemaError list.Schema.admitconstructs a domain model from a structured draft while preserving its fields.validateadds executable Schema behavior that no interpreter can read as metadata.
See Define Refined Types for domain-side definitions.