Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultHandwritten Versioned Contracts
A Contract<'model> is a chain of frozen wire schemas and explicit migrations. Use it when stored configuration,
queued messages, or events must remain readable after their wire shape changes. Contract versioning belongs at the
wire boundary; map the current wire value into a strict domain type afterwards.
This page declares a contract by hand. For generating the version series from [<DeriveSchema>] records, see
Generated version series. Schema derivation itself is documented separately
in Derived Schemas.
Wire and domain models
A wire model describes what the format can carry. Keep it public and permissive. A domain model describes what business code may rely on and should protect its invariants with refined values or private construction.
// Wire DTO: shaped like persisted input.
type OrderWire = { Sku: string; Quantity: int }
// Domain constructor owns business invariants.
// Order.create : string -> int -> Result<Order, OrderError>
let toDomain (wire: OrderWire) =
Order.create wire.Sku wire.QuantityParse or migrate to the current wire model first, then call toDomain. See
Separate Wire and Domain Models.
Declare a contract by hand
Contract.create starts with the current version and schema. Add each immediately preceding version with
Contract.supersedes, then choose how the input version is discovered with Contract.build.
open Reified
open Reified.SchemaDSL
type ConfigV1 = { Host: string }
let configV1Schema : Schema<ConfigV1> =
schema<ConfigV1> {
field _.Host
construct (fun host -> { Host = host })
}
type Config = { Host: string; Port: int }
let configSchema : Schema<Config> =
schema<Config> {
field _.Host
field _.Port
construct (fun host port -> { Host = host; Port = port })
}
let migrateV1ToV2 (v1: ConfigV1) : Result<Config, MigrationError> =
match v1.Host.Split ':' with
| [| host; port |] ->
match System.Int32.TryParse port with
| true, parsed -> Ok { Host = host; Port = parsed }
| false, _ -> Error(MigrationError.MigrationFailed $"unreadable port in '{v1.Host}'")
| _ -> Ok { Host = v1.Host; Port = 5432 }
let configContract : Contract<Config> =
Contract.create "Config" 2 configSchema
|> Contract.supersedes 1 configV1Schema migrateV1ToV2
|> Contract.build (VersionSource.Field "schemaVersion")
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.
FsLiveDocsGeneratedPage30_10EA2B4D0E31.ConfigV1Host: stringstringAn abbreviation for the CLI type . Basic Types
configV1Schema: Schema<ConfigV1>Reified.Schema`1Describes a typed value's portable structure and construction for schema interpreters. A schema records shape and construction metadata without tying that metadata to input parsing, diagnostics, validation, codecs, UI generation, or workflow execution. Primitive, collection, optional, union, refined, and record declarations all produce Schema<'value>. Record declarations use the schema<'value> { } computation expression. Each field may contain withSchema, constrain, constraints, refine, and validate operations before the declaration finishes with construct or constructResult.
schema: SchemaCeBuilder.SchemaBuilder<'model>Record-schema computation expression.
``.ctor``: Quotations.Expr<(ConfigV1 -> string)> -> field<ConfigV1,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"
_arg1: ConfigV1construct: 'constructor -> SchemaCeBuilder.ConstructorStep<'model,'constructor>Closes a record schema with a total constructor.
host: stringFsLiveDocsGeneratedPage30_10EA2B4D0E31.ConfigPort: intintAn abbreviation for the CLI type . Basic Types
configSchema: Schema<Config>``.ctor``: Quotations.Expr<(Config -> string)> -> field<Config,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"
_arg1: Config``.ctor``: Quotations.Expr<(Config -> int)> -> field<Config,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: Configport: intmigrateV1ToV2: ConfigV1 -> Result<Config,MigrationError>v1: ConfigV1Microsoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
Reified.MigrationErrorA failure produced while migrating an older wire representation.
Split: char * System.StringSplitOptions -> string arrayport: stringSystemTryParse: string * byref<int> -> boolConverts the string representation of a number to its 32-bit signed integer equivalent. A return value indicates whether the conversion succeeded. A string containing a number to convert. When this method returns, contains the 32-bit signed integer value equivalent of the number contained in , if the conversion succeeded, or zero if the conversion failed. The conversion fails if the parameter is or , is not of the correct format, or represents a number less than Int32.MinValue or greater than Int32.MaxValue. This parameter is passed uninitialized; any value originally supplied in will be overwritten. if was converted successfully; otherwise, .
System.Int32Represents a 32-bit signed integer.
parsed: intOkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
MigrationFailedconfigContract: Contract<Config>Reified.Contract`1A versioned wire contract whose successful result is the current domain model.
Reified.ContractFunctions for declaring and parsing explicitly versioned wire contracts.
create: string -> int -> Schema<'model> -> ContractBuilder<'model,'model>Starts a contract at its current version and schema.
(|>): '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
supersedes: int -> Schema<'previous> -> ('previous -> Result<'current,MigrationError>) -> ContractBuilder<'model,'current> -> ContractBuilder<'model,'previous>Adds the immediately preceding wire version and its typed migration to the next registered version.
build: VersionSource -> ContractBuilder<'model,'oldest> -> Contract<'model>Finishes a contract with an explicit version-detection strategy.
Reified.VersionSourceThe explicit source of a contract version.
FieldMigrations are hand-written, typed, contiguous, and may fail. Parsing selects the frozen schema for the input version,
parses it, then runs each adjacent migration in order, so every intermediate version is visited and none are skipped.
When the input version was not already current, the migrated result is re-checked against the current schema and a
failure becomes MigrationError.RevalidationFailed. When the input version was already current, the initial parse is
the check, so no second pass runs. There is no automatic structural migration.
Version sources
Contract.build chooses how the input version is discovered. The three cases are listed in the
VersionSource API reference; the example above reads a positive integer from a
schemaVersion input field. Use VersionSource.External when the caller knows the version out of band and calls
Contract.parseVersion, or VersionSource.UnversionedMeans 1 to treat input without a marker as one registered
version.
match Contract.parse configContract raw with
| Ok config -> printfn $"%s{config.Host}:%d{config.Port}"
| Error ContractError.VersionMissing -> eprintfn "no readable schema version"
| Error (ContractError.VersionUnrecognized version) -> eprintfn $"version %d{version} is not registered"
| Error (ContractError.VersionTooNew(found, supported)) ->
eprintfn $"payload is v%d{found}; this build supports v%d{supported}"
| Error (ContractError.ParseFailed(version, diagnostics)) ->
eprintfn $"v%d{version} payload is malformed: %A{diagnostics}"
| Error (ContractError.Migration failure) -> eprintfn $"migration failed: %A{failure}"ContractError.ParseFailed and MigrationError.RevalidationFailed carry the same path-aware SchemaErrors used by
Schema.parse. A MigrationError.MigrationFailed written by your own migration carries only its message string, so
no paths are preserved through hand-written migration failures.
Design rules
- Keep every shipped wire schema frozen.
- Write and test each adjacent migration; do not infer renames or defaults automatically.
- Keep generated records at the wire tier and map the current value into the domain.
- Revalidation after migration ensures the result passes the current schema.
- Generated output is ordinary Schema DSL code; contract versioning does not introduce runtime reflection.