Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result

Input Sources

Data is the source-agnostic boundary shape: Null, Text, Number, Bool, List, and Object. Adapters turn common sources into Data, and one schema parses them all. (Data is its own dependency-free package, useful beyond schemas, see its docs.)

The Schema

Verified
open Reified
open Reified.SchemaDSL
open Reified.ConstraintDSL

type Address = { City: string }
type Contact = { Kind: string; Value: string }

type Customer =
    { Name: string
      Address: Address
      Contacts: Contact list }

let addressSchema =
    schema<Address> {
        field _.City
        construct (fun city -> { City = city })
    }

let contactSchema =
    schema<Contact> {
        field _.Kind
        field _.Value
        construct (fun kind value -> { Kind = kind; Value = value })
    }

let customerSchema =
    schema<Customer> {
        field _.Name
        field _.Address {
            withSchema addressSchema
        }
        field _.Contacts {
            withSchema (Schema.listWith contactSchema)
            constrain (minLength 1)
        }
        construct (fun name address contacts ->
            { Name = name; Address = address; Contacts = contacts })
    }
Requires & types
Requires: open Reified; open Reified.ConstraintDSL; open Reified.SchemaDSL (packages: Reified.Constraint, Reified.Schema)
val addressSchema: Schema<Address>
val contactSchema: Schema<Contact>
val customerSchema: Schema<Customer>

Here addressSchema and contactSchema are intentionally local value schemas. If Address and Contact declare canonical intrinsic schemas, the Address line can use ordinary field, and the Contacts line can keep its field block while omitting only withSchema: the list still resolves through Contact's canonical schema, but the block's constrain (minLength 1) stays attached to the list field. That constraint constrains the length of the Contacts list, so it belongs on the list field, not on Contact itself, which is a single record. A bare field _.Contacts would drop the length check along with withSchema.

Nested fields expect object-shaped input and prefix their diagnostics with the field name; collection fields expect Data.List, parse every item, accumulate every item error, and prefix diagnostics with the item index.

Each adapter below produces Data. The closing parse example uses the configuration input; the other examples are isolated so their raw bindings do not collide.

HTTP Form-Like Input

Form posts and query strings are name/value pairs; repeated names become Data.List values:

Isolated
open Reified

let raw =
    Data.ofNameValues
        [ "name", "Ada Lovelace"
          "tag", "vip"
          "tag", "beta" ]      // repeated names accumulate into Data.List

Data.ofMap handles single-valued maps, and Data.ofNameValueCollection adapts System.Collections.Specialized.NameValueCollection directly from ASP.NET-style APIs. Name/value sources are flat; use the configuration or JSON adapters below when the input carries nested models or indexed collections.

CLI Arguments

Isolated
open Reified

let raw = Data.ofCliArgs [ "--name"; "Ada Lovelace"; "--verbose"; "--no-color" ]

--name value, --name=value, -n value, boolean flags, --no-name, and repeated options are supported; positional arguments collect under the _ field.

JSON Bodies With System.Text.Json

On .NET 8+ targets, adapt a parsed JsonDocument or JsonElement directly, the natural fit for ASP.NET Core request bodies:

Fragment
use! document = JsonDocument.ParseAsync request.Body
let raw = Data.ofJsonDocument document

JSON null, numbers, Booleans, arrays, and objects retain their corresponding Data cases. Number tokens keep their exact lexical representation. The adapter uses the in-box System.Text.Json, so the package stays dependency-free.

Parse JSON portably

For the same lossless behavior on .NET and Fable, use Json.parseData from the installed Reified.Schema package:

Isolated
open Reified

let raw = Json.parseData """{"name":"Ada","score":1.20e+3}"""

Under Fable, JSON.parse |> Data.ofJsonValue is also available when native JavaScript semantics are acceptable. That route has already discarded duplicate fields and the original spelling of number tokens; use Json.parseData when either distinction matters.

Configuration

Configuration keys use :-separated sections and numeric segments for collection indexes:

Verified
let raw =
    Data.ofConfiguration
        [ "name", "Ada Lovelace"
          "address:city", "London"
          "contacts:0:kind", "email"
          "contacts:0:value", "ada@example.com" ]
Requires & types
Requires: open Reified (packages: Reified.Data)
val raw: Data

Later pairs override earlier ones at the same path, matching .NET configuration layering: a repeated key keeps its last value, and a later scalar or section replaces the earlier shape at that key. A section path written after a scalar at the same key replaces the scalar with the section, and a later scalar at a section key replaces the section; the one exception is a section key whose value is null, as IConfiguration.AsEnumerable() emits alongside a section's children — that never overrides the children, so real layered IConfiguration output round-trips directly. Collections come from numeric segments, never from repetition; repeated names as multi-value input is a wire convention that belongs to ofNameValues. Numeric segments need not be contiguous: a sparse set such as contacts:0 and contacts:2 becomes a three-item list with Data.Null at the missing index 1.

One Parse For All Of Them

Continuing with the configuration input above:

Verified
let parsed = Schema.parseRetainingInput customerSchema raw

match parsed.Result with
| Ok customer -> ignore customer
| Error diagnostics ->
    // same paths for every source:
    parsed.ErrorsFor "contacts[0].value" |> List.map SchemaError.render |> ignore
Requires & types
Requires: open Reified (packages: Reified.Schema)
val parsed: RetainedParseResult<Customer>

From C#

Consume-don't-author: F# declares the schema, C# parses and reads diagnostics. Most adapters take plain .NET types already, ofNameValueCollection takes NameValueCollection, ofCliArgs and ofData take ordinary sequences and values, and call as plain static methods. ofMap and ofConfiguration take F#-only types (Map and a sequence of F# tuples), so use their C#-friendly equivalents instead:

using Reified.Schema;

// ofMap's C# equivalent, takes IDictionary<string, string> instead of an F# Map:
Data raw = DataModule.ofDictionary(new Dictionary<string, string> { ["name"] = "Ada Lovelace" });

// ofConfiguration's C# equivalent, takes the pairs IConfiguration.AsEnumerable() already returns:
Data fromConfig = DataModule.ofConfigurationPairs(configuration.AsEnumerable());

RetainedParseResult<Customer, SchemaError> parsed = Schema.parseRetainingInput(customerSchema, raw);

if (parsed.IsValid)
{
    Customer customer = parsed.Value;
}
else
{
    var errors = parsed.Errors; // FSharpList<Diagnostic<SchemaError>>
}

Schema.parseWith takes an F# function for its configure parameter, which C# cannot pass a lambda to directly. Use Schema.parseWithOptions, which takes a Func<Options, Options> instead:

var parsed = Schema.parseWithOptions(o => Schema.constructorErrorAt("end", o), dateRangeSchema, raw);