Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultBoundary-specific field rules
A refinement holds for every value of its type. A constraint holds at one boundary. Use a constraint on a field block when the rule belongs to this form rather than to the domain type. SchemaDSL lists the full field block vocabulary; Refined Schemas shows how to lift a rule that should hold everywhere into a refinement.
open Reified
open Reified.SchemaDSL
open Reified.ConstraintDSL
type Profile =
{ DisplayName: string
Age: int }
let profileSchema =
schema<Profile> {
field _.DisplayName {
constraints [ present; maxLength 40 ]
}
field _.Age {
constrain (Constraint.between 13 120)
}
construct (fun displayName age -> { DisplayName = displayName; Age = age })
}
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
FsLiveDocsGeneratedPage7_76B7E1D94D35.ProfileDisplayName: stringstringAn abbreviation for the CLI type . Basic Types
Age: intintAn abbreviation for the CLI type . Basic Types
profileSchema: Schema<Profile>schema: SchemaCeBuilder.SchemaBuilder<'model>Record-schema computation expression.
``.ctor``: Quotations.Expr<(Profile -> string)> -> field<Profile,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: ProfileConstraints: SchemaFieldSteps.FieldInitial<Profile,string> * Constraint<string> list -> SchemaFieldSteps.FieldConfigured<Profile,string>Adds portable constraints to the field's inferred schema in declaration order.
present: Constraint<^value>Alias for .
maxLength: int -> Constraint<^value>Alias for .
``.ctor``: Quotations.Expr<(Profile -> int)> -> field<Profile,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: ProfileConstrain: SchemaFieldSteps.FieldInitial<Profile,int> * Constraint<int> -> SchemaFieldSteps.FieldConfigured<Profile,int>Adds a portable constraint to the field's current schema value.
Reified.ConstraintModuleCreates, executes, composes, and inspects constraints.
between: 'value -> 'value -> Constraint<'value>Requires a value inside the supplied inclusive bounds. let retryCount : Constraint<int> = Constraint.between 0 10
construct: 'constructor -> SchemaCeBuilder.ConstructorStep<'model,'constructor>Closes a record schema with a total constructor.
displayName: stringage: intA field block is the expanded form of field _.DisplayName. constrain adds one; constraints adds a list.
Constraints reach the interpreters just as refinements do:
JsonSchema.generate profileSchema
// "displayName": {"type":"string","maxLength":40}
// "age": {"type":"integer","minimum":13,"maximum":120}
Reified.JsonSchemaGenerates JSON Schema documents from built model schemas. The generator is a pure interpreter over descriptions: it lowers shapes, declared formats, and portable constraint metadata to JSON Schema keywords without parsing input, running checks, or constructing models. One schema declaration therefore drives parsing, validation, and the published contract. Lowering rules: primitives map to type (with format for dates, date-times, and uuids), refined values lower to their underlying primitive representation, nested models to object with properties and required, collections to array with items, maps to object with additionalProperties, and tagged unions to oneOf with a const-constrained discriminator property per case. An option lowers to its payload in an omittable record field, where the codec leaves None out, and to anyOf of its payload and null everywhere the codec writes None as null. Constraint metadata lowers to minLength, maxLength, pattern, enum, minimum/maximum (and exclusive variants), multipleOf, minItems/maxItems, and uniqueItems; constraints without a JSON Schema equivalent, such as trimmed, are skipped. Default-value metadata attached with Schema.withDefault lowers to default.
generate: Schema<'model> -> stringGenerates a compact JSON Schema document from any completed schema declaration. The record, primitive, collection, union, or other completed schema to lower. Thrown when is null. open Reified.SchemaDSL open Reified.ConstraintDSL type Customer = { Name: string; Email: string } let customerSchema = schema<Customer> { field _.Name { constrain present } field _.Email { constraints [ present; email ] } construct (fun name email -> { Name = name; Email = email }) } let document = JsonSchema.generate customerSchema // {"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{...},"required":[...]}
profileSchema: Schema<Profile>displayName: This value must be present.
age: Must be between 13 and 120; got 9.
A constraint preserves the value's type, maxLength 40 on a string field leaves a string field. That is the
difference from refine, which changes the type and is what makes the guarantee durable.
Which to reach for:
- The rule is true of every value of the type, put it in the refinement, and every construction path enforces it.
- The rule is true only at this boundary, put it in the field block, where a reader can see it applies here.
Schema will not take metadata without an executable check behind it, so what an inspector reports is always what parsing enforces. Constraint names come from Constraints; see Refined Schemas for application-defined constraints.