Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result
open Reified.Schema.JsonSchema Inference
schemagen parses F# source and lowers marked declarations to typed Schema DSL. It does not load your assembly or
inspect types at runtime.
Record shape
A derivable record must be:
- public and declared at namespace level;
- non-generic;
- in a file with one namespace for generated declarations; and
- marked with
[<DeriveSchema>].
Every record field becomes one schema field in declaration order. By default the generated construct expression
creates a record literal. A single [<SchemaConstructor>] static member replaces that literal; it must accept fields in
declaration order and return the record type.
The generated module uses the record's type name and exposes schema, parse, and validate.
Generated code refers to your fields by name. Adding, removing, renaming, or changing a field without regenerating makes
F# compilation fail rather than allowing the schema to drift.
Field type inference
| F# field type | Inferred schema |
|---|---|
string |
Schema.text |
int |
Schema.int |
decimal |
Schema.decimal |
bool |
Schema.bool |
DateOnly |
Schema.date |
DateTimeOffset |
Schema.dateTime |
Guid |
Schema.guid |
'a option |
optional field using the inferred 'a schema |
'a list |
Schema.listWith the inferred element schema |
Map<string, 'a> |
Schema.mapWith the inferred value schema |
| another marked record in the same file | that record's generated schema |
| a nullary discriminated union | enum schema; case names become tags |
a [<DeriveUnion>] union |
internally tagged union using type, with fieldless, directly named, and marked-record payload cases |
a [<DeriveUnion "field">] union |
the same internally tagged union with a custom discriminator |
The wire vocabulary is intentionally closed. Arrays, tuples, generic records, nested options, floating-point types,
non-string map keys, other integer widths, and unknown application types produce generation diagnostics. Use list,
decimal, int, or an explicitly mapped domain boundary instead of silently changing wire semantics.
Marked-record references stay within one source file so generation and F# compile ordering remain deterministic.
Names
The default naming policy is camel, so MarketingOptIn becomes marketingOptIn. Set
ReifiedSchemaNaming to snake or verbatim for the whole project. Use
[<SchemaName "marketing_opt_in">] on one field or nullary union case to override the policy locally.
Options, supplied fields, and defaults
An option field represents wire absence and parses an omitted key as None. There is no second nested-option absence
axis. [<Supplied>] is different: it requires the key to occur in the input even when the typed value could otherwise
be produced. [<Default value>] supplies an omitted non-optional field and cannot be used on an option field.
Constraints and metadata
Field attributes are lowered in source order to the operations documented in the
attribute table. Constraints remain executable and inspectable, so parsing,
Schema.check, JSON Schema, forms, and diagnostics all observe the same rule. Format is metadata only. XML ///
comments become Schema.describe metadata and generated XML documentation.
Union inference
A union whose cases have no payload can be used as a field type without marking the union:
type Plan = Free | Team | Enterprise
[<DeriveSchema>]
type Signup = { Plan: Plan }Tags follow the naming policy and can be overridden with [<SchemaName>] on a case.
Payload unions opt into generation with [<DeriveUnion>]. A fieldless case may be mixed with directly named case
fields or cases carrying one marked record from the same file:
[<DeriveSchema>]
type Card = { LastFour: string }
[<DeriveSchema>]
type Bank = { Iban: string }
[<DeriveUnion>]
type Payment =
| Cash
| Credit of limit: decimal
| Card of Card
| Bank of Bank
[<DeriveSchema>]
type Checkout = { Payment: Payment }The generated schema follows the handwritten union schema rules.
Every direct case field must be named. Credit of decimal is rejected instead of publishing the compiler-generated
wire name item.
Version-series inference
Names ending in Vn are grouped by convention. For example, ProfileV1, ProfileV2, and a bare Profile form a
contiguous series, with the bare record inferred as the current version. Use
[<DeriveSchema(Chain = "Profile", Version = 1)>] when names do not follow the convention. A series additionally gets
a typed contract builder whose parameters are the explicit migrations between adjacent versions; see
Versioned Contracts.