Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultReified vs zod
Reified's schema group is the same idea zod made mainstream: declare the shape once, parse unknown input through it, and get either a typed value or structured, path-aware issues. The differences come from F# and .NET.
The Same Moves
Table entries on the right are shorthand for the complete declarations shown in
Schema quickstart and Union schemas. maxLength,
constrain, and present come from Reified.ConstraintDSL; schema, field, construct, and Schema.union come
from Reified.SchemaDSL.
| zod | Reified |
|---|---|
z.object({ name: z.string().max(80) }) |
schema<...> { field _.Name { constrain (maxLength 80) }; construct ctor } |
schema.safeParse(input) |
Schema.parse schema raw → Result |
result.error.issues with paths |
SchemaErrors.toList errors and SchemaPath.format issue.Path |
z.string().email().brand<'Email'>() |
Schema.refine over a private representation and constraint-backed Refinement |
z.discriminatedUnion("type", ...) |
Schema.union [ UnionCase.fields ... ] |
z.infer<typeof schema> |
not needed, the schema is declared against your record type directly |
zod-to-json-schema |
JsonSchema.generate (in Reified.Schema, from the same metadata) |
zod derives the static type from the schema; Reified declares the schema against a type you own. Your domain type stays
an ordinary F# record with real members, and each field _.Property declaration names one of those members as its
getter. The computation expression tracks each field's type in declaration order, so the construct lambda's parameters
are checked against the getters' types at compile time: a missing, extra, reordered, or mistyped parameter is a compile
error, not a runtime failure.
What Reified Adds Beyond Parsing
- Trusted-path serialization:
Json.compileturns the same declaration into a compiled JSON codec measured againstSystem.Text.Jsonon a representative customer aggregate and a 24-field record: decoding was faster and serialization roughly matched in local runs (see Benchmarks for the workload and versions). zod validates on the way in but does not own the way out. - Redisplay: failed parses keep the structured data, so form fields re-render with the user's values next to their errors without extra state.
- Workflow policies: environment-aware requirements over already-trusted models, applied per workflow.
Runtime Differences That Matter
- No reflection: Reified schemas are explicit declarations compiled into plans; nothing depends on runtime type inspection, so NativeAOT and aggressive trimming work by construction.
- Fable: the schema core, including its
Jsoncodecs, compiles to JavaScript through Fable, so the browser and the server can share one declaration, encode and decode included, the role zod plays across the TypeScript stack. See Compiler-Directed, AOT, and Fable for the supported targets. - Errors are values with rendering:
SchemaErroris a typed union with a default English renderer (RetainedParseResult.renderErrors) and a one-function mapping into your own error union (Result.mapErroroverparsed.Result), rather than a bag of issue objects.
Schema.parseRetainingInput produces the retained parse result those one-liners name:
open Reified
open Reified.SchemaDSL
open Reified.ConstraintDSL
type Signup = { Email: string; Age: int }
let signupSchema =
schema<Signup> {
field _.Email { constrain email }
field _.Age { constrain (atLeast 13) }
construct (fun email age -> { Email = email; Age = age })
}
let parsed =
Schema.parseRetainingInput signupSchema
(Data.ofNameValues [ "email", "ada@example.com"; "age", "12" ])
parsed.IsValid // false
RetainedParseResult.renderErrors parsed
// [ "age: Expected a value at least 13, but was 12." ]
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
FsLiveDocsGeneratedPage26_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: SignupConstrain: SchemaFieldSteps.FieldInitial<Signup,string> * Constraint<string> -> SchemaFieldSteps.FieldConfigured<Signup,string>Adds a portable constraint to the field's current schema value.
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: intparsed: RetainedParseResult<Signup>Reified.SchemaConstruction, composition, parsing, and checking for universal schemas.
parseRetainingInput: Schema<'a> -> Data -> RetainedParseResult<'a>Parses source-neutral structured data while retaining it for redisplay and error lookup.
Reified.DataModuleofNameValues: (string * string) seq -> DataBuilds object-shaped data from name and value pairs.
IsValid: boolReturns true when input parsing produced a trusted model.
Reified.RetainedParseResultFunctions for creating and rendering retained schema parse results.
renderErrors: RetainedParseResult<'value> -> string listRenders one line for every failed schema issue.
Requires & types
Requires: open Reified; open Reified.ConstraintDSL; open Reified.SchemaDSL (packages: Reified.Constraint, Reified.Data, Reified.Schema)val signupSchema: Schema<Signup>val parsed: RetainedParseResult<Signup>Where zod Fits Better
zod fits TypeScript-first codebases. For F#/.NET, Reified's schema group covers structured parsing; code that does not need a schema can use plain Result.