Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultChoosing an API
Five operations read a declaration. They differ in the input they accept, the validation they run, and the error they return. Pick by where the input came from and whether the producer is trusted.
| Operation | Input | Validation run | Constructor | Error type | Trust the payload? |
|---|---|---|---|---|---|
Schema.parse |
Data |
Field decoding, constraints, refinements, and constructor invariants | Runs only after every field succeeds | SchemaErrors (accumulated, path-aware) |
No — the untrusted boundary |
Schema.check |
an existing 'model |
Same field rules and constructor as Schema.parse, read back from the value |
Re-invoked with the checked fields; returns the original value on success | SchemaErrors (accumulated, path-aware) |
No — the value's history is uncertain |
Json.deserialize |
JSON string |
Wire shape and required fields only; constraint metadata is skipped | Runs; a checked constructor failure raises | raises JsonCodecException |
Yes — storage, internal service, cache |
Json.tryDeserialize |
JSON string |
Same as Json.deserialize |
Same as Json.deserialize |
Result<'model, string> with the first structural failure |
Yes — same trusted path, non-throwing |
Contract.parse |
Data |
Version selection, that version's schema parse, migrations, then a Schema.check of the migrated value |
Via each version's parse and the final recheck | ContractError |
No — old wire shapes must stay readable |
Json.parseData is the bridge between the first and third rows: it turns JSON text into Data with no schema
applied. Schema.parseRetainingInput wraps Schema.parse and keeps the original Data for redisplay and auditing.
open Reified
open Reified.SchemaDSL
open Reified.ConstraintDSL
type Signup = { Email: string; Age: int }
let signupSchema =
schema<Signup> {
field _.Email { constraints [ present; email ] }
field _.Age { constrain (atLeast 13) }
construct (fun email age -> { Email = email; Age = age })
}
let data = Data.ofNameValues [ "email", "ada"; "age", "11" ]
Schema.parse signupSchema data
// Result<Signup, SchemaErrors>
Schema.check signupSchema { Email = "ada"; Age = 11 }
// Result<Signup, SchemaErrors>
let codec = Json.compile signupSchema
Json.tryDeserialize codec """{"email":"ada@example.org","age":36}"""
// Ok signup — constraint metadata is not run on the trusted path
Json.tryDeserialize codec """{"email":"ada@example.org","age":"thirty"}"""
// Error "JSON decode failed at $.age: expected digit"
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
FsLiveDocsGeneratedPage21_9A5DDEA924C8.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: 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 .
construct: 'constructor -> SchemaCeBuilder.ConstructorStep<'model,'constructor>Closes a record schema with a total constructor.
email: stringage: intdata: DataReified.DataModuleofNameValues: (string * string) seq -> DataBuilds object-shaped data from name and value pairs.
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.
check: Schema<'a> -> 'a -> Result<'a,SchemaErrors>Checks an existing typed value, such as a freely constructed draft, through the schema's constraints, refinements, and record constructor.
codec: 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
tryDeserialize: JsonCodec<'model> -> string -> Result<'model,string>Deserializes a JSON string, returning decode failures as a rendered message instead of raising. Thrown when or 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 data: Dataval codec: JsonCodec<Signup>When the same payload needs different treatment
- A request body from a client you do not control goes through
Json.parseData(orJson.tryParseData) intoData, thenSchema.parse— never throughJson.deserialize, which would skip the rules. - A value read back from your own database or another service you trust can use
Json.deserialize/Json.tryDeserialize. If the payload carries constraint attributes that matter, remember the trusted path does not read them. - A value produced by a serializer, mapper, or import goes through
Schema.check, which re-runs the rules on a value that already exists. - A payload whose shape has shipped in several versions goes through
Contract.parse, which selects the version, parses, migrates, and rechecks the current model.
The same trust distinction is stated once in JSON codecs and in the Schema or Generated DTO decision.