Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultInspecting Schema Metadata Tutorial
Inspect.model reads a schema's fields, shapes, formats, and constraints without parsing input or constructing a model.
JSON Schema emitters, documentation generators, and UI tools can use this metadata.
Describe A Schema
Inspect.model turns a built schema into a plain metadata tree:
open Reified
open Reified.SchemaDSL
open Reified.ConstraintDSL
type Signup = { Email: string; Age: int }
let signupSchema =
schema<Signup> {
field _.Email {
withSchema (Schema.text |> Schema.withFormat SchemaFormat.email)
}
field _.Age
construct (fun email age -> { Email = email; Age = age })
}
let description = Inspect.model signupSchema
description.Fields |> List.map _.Name // ["email"; "age"]
description.Fields |> List.map _.Order // [0; 1]
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
FsLiveDocsGeneratedPage5_76B7E1D94D35.SignupEmail: stringstringAn abbreviation for the CLI type . Basic Types
Age: intintAn abbreviation for the CLI type . Basic Types
signupSchema: Schema<Signup>schema: SchemaCeBuilder.SchemaBuilder<'model>Record-schema computation expression.
``.ctor``: Quotations.Expr<(Signup -> string)> -> field<Signup,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: SignupWithSchema: SchemaFieldSteps.FieldInitial<Signup,string> * Schema<'current> -> SchemaFieldSteps.FieldWorking<Signup,string,'current>Supplies the schema transformed by the remaining operations in this field block.
Reified.SchemaConstruction, composition, parsing, and checking for universal schemas.
text: Schema<string>Describes text input.
(|>): '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
withFormat: SchemaFormat -> Schema<'a> -> Schema<'a>Adds format metadata.
Reified.SchemaFormatModuleFunctions for creating and inspecting schema value format metadata.
email: SchemaFormatThe well-known email address format.
``.ctor``: Quotations.Expr<(Signup -> int)> -> field<Signup,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: Signupconstruct: 'constructor -> SchemaCeBuilder.ConstructorStep<'model,'constructor>Closes a record schema with a total constructor.
email: stringage: intdescription: ModelDescriptionReified.InspectThe inspection API over built schemas and value schemas. Inspect is the entry point for non-validation interpreters: JSON Schema generation, documentation, UI metadata, and codec planning all start from the same descriptions. The returned trees are plain immutable data — inspecting them never parses input, runs checks, or constructs models.
model: Schema<'model> -> ModelDescriptionDescribes a built model schema as inspectable field metadata. The built model schema to describe. Thrown when is null. Thrown when is not a completed model schema. let description = Inspect.model customerSchema let names = description.Fields |> List.map _.Name
Fields: FieldDescription listThe field descriptions in declared order.
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 ]
_arg1: FieldDescriptionName: stringThe canonical boundary-facing external field name.
Order: intThe zero-based field order used for trusted construction and ordered interpreter output.
Nothing executes: no getters run, no constructors are called, no checks fire. The description is immutable data.
Read Field Shapes And Constraints
Each field carries a SchemaDescription, shape, declared format, and constraint metadata:
let email = description.Fields |> List.find (fun field -> field.Name = "email")
email.Schema.Format // Some SchemaFormat.email (when declared with Schema.withFormat)
match email.Schema.Shape with
| SchemaShape.Primitive kind -> printfn "primitive %A" kind
| SchemaShape.Refined underlying -> printfn "refined over %A" underlying.Shape
| SchemaShape.Nested model -> printfn "nested with %d fields" model.Fields.Length
| SchemaShape.Many item -> printfn "collection of %A" item.Shape
| SchemaShape.Union union -> printfn "union with cases %A" (union.Cases |> List.map _.Tag)Refined values expose their raw representation through SchemaShape.Refined, so a boundary interpreter can render an
Email field as a constrained string without knowing the domain type.
Understand The Constraint Types
There is one constraint type. Schema.constrain takes the same Reified.Constraint.Constraint<'value> you would check
directly, and inspection hands back the same ConstraintDescription that Constraint.inspect returns. Schema erases
the value type when it stores a constraint in its heterogeneous field plan, but that erasure is internal: nothing in
the public inspection surface is Schema-specific.
Boundary supply is separate, because it is decided before a typed value exists. Schema.mustSupply and
Schema.mayOmit record it, and inspection exposes it as Supply beside the constraints rather than mixed in among
them.
Lower Constraints To Another Format
A description is a small recursive tree, atoms, All, Any, Optional, and Opaque, and every interpreted atom
reuses the same expectation types the violations use, so lowering is one traversal:
open Reified
let jsonKeyword (atom: ConstraintAtom) =
match atom with
| CardinalityAtom (Cardinality.Maximum maximum) -> Some $"\"maxLength\":{maximum}"
| FormatAtom (Pattern pattern) -> Some $"\"pattern\":\"{pattern}\""
| _ -> None
let keywords (description: SchemaDescription) =
description.Constraints
|> List.collect ConstraintDescription.atoms
|> List.choose jsonKeyword
ReifiedjsonKeyword: ConstraintAtom -> string optionatom: ConstraintAtomReified.ConstraintAtomOne interpreted primitive: the complete semantic identity of a built-in constraint. An interpreted constructor builds exactly one atom and places that same value in both its description and any violation it produces, so a primitive's identity and its failure cannot drift. Atoms are shape-neutral; an interpreter combines an atom with the surrounding schema shape to decide what it lowers to.
CardinalityAtomA text length or collection count rule.
Reified.CardinalityWhat a size rule expects of a text length or collection count. Shape-neutral. An interpreter combines it with the schema shape to reach maxLength, maxItems, or maxProperties. Text sizes count Unicode code points, not UTF-16 code units.
MaximumAt most the supplied size.
maximum: intSomeThe representation of "Value of type 'T" The input value. An option representing the value.
FormatAtomA built-in text format rule.
PatternA match for the supplied .NET regular expression.
pattern: stringNoneThe representation of "No value"
keywords: SchemaDescription -> string listdescription: SchemaDescriptionReified.SchemaDescriptionDescribes one value schema: its shape, declared format, and portable constraint metadata.
Constraints: ConstraintDescription listThe constraint descriptions attached to this value schema layer, in declaration order.
(|>): '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.
collect: ('T -> 'U list) -> 'T list -> 'U listFor each element of the list, applies the given function. Concatenates all the results and return the combined list. The function to transform each input element into a sublist to be concatenated. The input list. The concatenation of the transformed sublists. For each positive number in the array we are generating all the previous positive numbers [1..4] |> List.collect (fun x -> [1..x]) The sample evaluates to [1; 1; 2; 1; 2; 3; 1; 2; 3; 4] (added extra spaces for easy reading)
Reified.ConstraintDescriptionModuleBuilds and traverses constraint descriptions.
atoms: ConstraintDescription -> ConstraintAtom listEvery interpreted primitive reachable without crossing an opacity boundary, in authored order. Use this only where dropping an unexportable sibling stays sound. Dropping a conjunct weakens a conjunction and dropping a disjunct strengthens a disjunction, so an interpreter that claims enforcement must consult the whole expression rather than this projection.
choose: ('T -> 'U option) -> 'T list -> 'U listApplies a function to each element in a list and then returns a list of values v where the applied function returned Some(v). Returns an empty list when the input list is empty or when the applied chooser function returns None for all elements. The function to be applied to the list elements. The input list. The resulting list comprising the values v where the chooser function returned Some(x). Using the identity function id (is defined like fun x -> x): let input1 = [ Some 1; None; Some 3; None ] input1 |> List.choose id Evaluates to [ 1; 3 ] type Happiness = | AlwaysHappy | MostOfTheTimeGrumpy type People = { Name: string; Happiness: Happiness } let takeJustHappyPersons person = match person.Happiness with | AlwaysHappy -> Some person.Name | MostOfTheTimeGrumpy -> None let candidatesForTheTrip = [ { Name = "SpongeBob" Happiness = AlwaysHappy } { Name = "Patrick" Happiness = AlwaysHappy } { Name = "Squidward" Happiness = MostOfTheTimeGrumpy } ] candidatesForTheTrip |> List.choose takeJustHappyPersons Evaluates to [ "SpongeBob"; "Patrick" ] let input3: int option list = [] input3 |> List.choose id Evaluates to: empty list let input4: string option list = [None; None] input4 |> List.choose id Evaluates to empty list Using the identity function id (is defined like fun x -> x): let input5 = [ Some 1; None; Some 3; None ] input5 |> List.choose id // evaluates [1; 3]
ConstraintDescription.atoms deliberately stops at an opacity boundary, and the keywords traversal above is only
safe where dropping a rule is sound; it does not claim to enforce the whole expression. An interpreter that claims
enforcement must consult the whole expression instead: dropping a conjunct weakens an All, and dropping a disjunct
strengthens an Any, which would reject values the library accepts. A conservative alternative recurses over
ConstraintDescription.Expression and refuses any form it cannot represent rather than flattening across it:
let rec expressionKeywords (description: ConstraintDescription) : string list option =
match description.Expression with
| ConstraintExpression.Atom atom -> jsonKeyword atom |> Option.map List.singleton
| ConstraintExpression.All children ->
children
|> List.map expressionKeywords
|> List.fold
(fun acc next ->
match acc, next with
| Some keywords, Some more -> Some(keywords @ more)
| _ -> None)
(Some [])
| ConstraintExpression.Optional inner -> expressionKeywords inner
// A disjunction or an opaque node cannot be flattened into one keyword list without
// changing what it accepts, so refuse and keep the rule as runtime metadata instead.
| ConstraintExpression.Any _
| ConstraintExpression.Opaque _ -> NoneThis shape recurses over the logical form: a conjunction keeps every branch, an optional delegates to its inner
constraint, and a disjunction or opaque node returns None so the caller retains it as runtime metadata rather than
publishing a keyword that enforces the wrong rule.
Atoms are shape-neutral. Cardinality.Maximum 5 becomes maxLength, maxItems, or maxProperties depending on the
SchemaShape it is attached to, so combine the two rather than reading the description alone.
The repository keeps three worked prototypes, a JSON Schema emitter, a docs describer, and a UI metadata producer,
in tests/Reified.Schema.Tests/SchemaInterpreterPrototypeTests.fs, all built only on Inspect.
Next
- Refined Value Schemas for how refinement layers stay inspectable.
- Schema reference for the full
InspectAPI.