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

One F# model for .NET and Fable JavaScript

Encode invariants once.
Enforce project wide.

Declare a rule once, on a value, a field, or a whole model, and the checking, the diagnostics, the JSON codec, the contract document, and the test data are all read from that one declaration. No runtime reflection: the same declaration compiles under NativeAOT, trimming, and Fable.

Verified
open Reified.SchemaDSL
open Reified.ConstraintDSL

type Signup = { Email: string; Age: int; Newsletter: bool }

let signupSchema =
    schema<Signup> {
        field _.Email { constraints [ present; email ] }
        field _.Age { constrain (atLeast 13) }
        field _.Newsletter
        construct (fun email age newsletter ->
            { Email = email; Age = age; Newsletter = newsletter })
    }

let input =
    Data.ofNameValues
        [ "email", "ada"
          "age", "11" ]

let result = Schema.parse signupSchema input
// age: Expected a value at least 13, but was 11.
// email: Expected an email address, but was ada.
// newsletter: This value was omitted.

let signup = { Email = "ada@example.org"; Age = 36; Newsletter = true }
let codec = Json.compile signupSchema     // compile once, typically at startup
Json.serialize codec signup
// {"email":"ada@example.org","age":36,"newsletter":true}
Requires & types
Requires: open Reified; open Reified.ConstraintDSL; open Reified.SchemaDSL (packages: Reified.Constraint, Reified.Data, Reified.Schema)
val signupSchema: Schema<Signup>
val input: Data
val result: Result<Signup,SchemaErrors>
val signup: Signup
val codec: JsonCodec<Signup>

The compiled codec is competitive with System.Text.Json on serialization and ahead of it on decoding: on canonical UTF-8 input, Reified's decoder ran about 14% faster on .NET 10 in the recorded benchmark, and allocated less, because a schema-derived codec skips per-call reflection and boxing.

Verified
JsonSchema.generate signupSchema
// {"type":"object",
//  "properties":{"email":{"type":"string"},
//                "age":{"type":"integer","minimum":13},
//                "newsletter":{"type":"boolean"}},
//  "required":["email","age","newsletter"]}
Requires & types
Requires: open Reified (packages: Reified.Schema)

Failure messages, the JSON codec, the JSON Schema, and generated test data come from the same declaration.

Get started →

  • JSON without reflection. A schema compiles to a JSON codec that runs under NativeAOT and trimming, and the same schema compiles to Fable JavaScript. See JSON Codecs and AOT, trimming, and Fable.
  • Inspect a schema. One declaration produces field-aware parse diagnostics, JSON Schema export, and messages a translator can localize without touching the rule. See Schema, Redisplay and Field Errors, and Localization.
  • Refined types. A value's invariant is proven once, at construction, and every later use relies on the proof instead of re-checking it. See Refined values.
  • Parse and Result. Untrusted text becomes a typed value with a reason attached to failure, and ordinary F# Result composes without an exception model underneath it. See Parsing and Result handling.
  • Data. Schema.parse reads from it, not from JSON, a form post, or a query string directly, so it is the one boundary-input shape untrusted data becomes on the way in, as well as a source-neutral value for building fixtures and comparing output. The trusted Json.compile codec path bypasses it entirely, string or bytes straight to your model, so it is required for the boundary path and optional for the trusted one, not optional everywhere. See Data.

Writing a schema for a payload you receive from somewhere else, or one with old versions still in storage? See Schema or Generated DTO before you start: generation and a hand-written schema solve different problems, and combining them wrong is the most common way to end up with validation that silently does not run.

Get started →

Packages and platforms →