Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultOne F# model for .NET and Fable JavaScript
Encode invariants once.
Enforce project wide.
Declare a rule once, on a value, a field, or a whole model, and the checking, the diagnostics, the JSON codec, the contract document, and the test data are all read from that one declaration. No runtime reflection: the same declaration compiles under NativeAOT, trimming, and Fable.
open Reified.SchemaDSL
open Reified.ConstraintDSL
type Signup = { Email: string; Age: int; Newsletter: bool }
let signupSchema =
schema<Signup> {
field _.Email { constraints [ present; email ] }
field _.Age { constrain (atLeast 13) }
field _.Newsletter
construct (fun email age newsletter ->
{ Email = email; Age = age; Newsletter = newsletter })
}
let input =
Data.ofNameValues
[ "email", "ada"
"age", "11" ]
let result = Schema.parse signupSchema input
// age: Expected a value at least 13, but was 11.
// email: Expected an email address, but was ada.
// newsletter: This value was omitted.
let signup = { Email = "ada@example.org"; Age = 36; Newsletter = true }
let codec = Json.compile signupSchema // compile once, typically at startup
Json.serialize codec signup
// {"email":"ada@example.org","age":36,"newsletter":true}
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
FsLiveDocsGeneratedPage1_0A9B158FC355.SignupEmail: stringstringAn abbreviation for the CLI type . Basic Types
Age: intintAn abbreviation for the CLI type . Basic Types
Newsletter: boolboolAn 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: SignupConstraints: SchemaFieldSteps.FieldInitial<Signup,string> * Constraint<string> list -> SchemaFieldSteps.FieldConfigured<Signup,string>Adds portable constraints to the field's inferred schema in declaration order.
present: Constraint<^value>Alias for .
email: Constraint<string>Alias for .
``.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: SignupConstrain: SchemaFieldSteps.FieldInitial<Signup,int> * Constraint<int> -> SchemaFieldSteps.FieldConfigured<Signup,int>Adds a portable constraint to the field's current schema value.
atLeast: 'a -> Constraint<'a>Alias for .
``.ctor``: Quotations.Expr<(Signup -> bool)> -> field<Signup,bool>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"
_arg3: Signupconstruct: 'constructor -> SchemaCeBuilder.ConstructorStep<'model,'constructor>Closes a record schema with a total constructor.
email: stringage: intnewsletter: boolinput: DataReified.DataModuleofNameValues: (string * string) seq -> DataBuilds object-shaped data from name and value pairs.
result: Result<Signup,SchemaErrors>Reified.SchemaConstruction, composition, parsing, and checking for universal schemas.
parse: Schema<'a> -> Data -> Result<'a,SchemaErrors>Parses source-neutral structured data, runs constraints and refinements, and invokes record constructors.
signup: Signupcodec: JsonCodec<Signup>Reified.JsonFunctions for compiling and running JSON codecs over built model schemas.
compile: Schema<'model> -> JsonCodec<'model>Compiles a completed schema into a reusable JSON codec. Compile once per schema, typically at startup, and reuse the codec for every value. Constructor-last object schemas retain a typed record plan, including checked constructors. Constructor failures surface as during decoding. Thrown when is null. Thrown when is incomplete. 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 customer = { Name = "Ada"; Email = "ada@example.org" } let codec = Json.compile customerSchema let json = Json.serialize codec customer let roundTripped = Json.deserialize codec json
serialize: JsonCodec<'model> -> 'model -> stringSerializes a trusted model to a JSON string through a compiled codec. Thrown when is null.
Requires & types
Requires: open Reified; open Reified.ConstraintDSL; open Reified.SchemaDSL (packages: Reified.Constraint, Reified.Data, Reified.Schema)val signupSchema: Schema<Signup>val input: Dataval result: Result<Signup,SchemaErrors>val signup: Signupval codec: JsonCodec<Signup>The compiled codec is competitive with
System.Text.Json on serialization and ahead of it on decoding: on canonical UTF-8 input, Reified's
decoder ran about 14% faster on .NET 10 in the recorded benchmark, and
allocated less, because a schema-derived codec skips per-call reflection and boxing.
JsonSchema.generate signupSchema
// {"type":"object",
// "properties":{"email":{"type":"string"},
// "age":{"type":"integer","minimum":13},
// "newsletter":{"type":"boolean"}},
// "required":["email","age","newsletter"]}
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":[...]}
signupSchema: Schema<Signup>Requires & types
Requires: open Reified (packages: Reified.Schema)Failure messages, the JSON codec, the JSON Schema, and generated test data come from the same declaration.
- JSON without reflection. A schema compiles to a JSON codec that runs under NativeAOT and trimming, and the same schema compiles to Fable JavaScript. See JSON Codecs and AOT, trimming, and Fable.
- Inspect a schema. One declaration produces field-aware parse diagnostics, JSON Schema export, and messages a translator can localize without touching the rule. See Schema, Redisplay and Field Errors, and Localization.
- Refined types. A value's invariant is proven once, at construction, and every later use relies on the proof instead of re-checking it. See Refined values.
- Parse and Result. Untrusted text becomes a typed value with a reason attached to failure, and ordinary F#
Resultcomposes without an exception model underneath it. See Parsing and Result handling. - Data.
Schema.parsereads from it, not from JSON, a form post, or a query string directly, so it is the one boundary-input shape untrusted data becomes on the way in, as well as a source-neutral value for building fixtures and comparing output. The trustedJson.compilecodec path bypasses it entirely, string or bytes straight to your model, so it is required for the boundary path and optional for the trusted one, not optional everywhere. See Data.
Writing a schema for a payload you receive from somewhere else, or one with old versions still in storage? See Schema or Generated DTO before you start: generation and a hand-written schema solve different problems, and combining them wrong is the most common way to end up with validation that silently does not run.