Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result
open Reified.Schema.JsonJSON Codecs
This page shows how Reified.Schema.Json turns the schema you already declared into a compiled JSON codec, so trusted
serialization and boundary parsing come from one declaration.
Reified has two paths for JSON, and they exist because they optimize for different things:
- Boundary parsing —
Data+Schema.parse: for untrusted input. It runs constraint metadata, accumulates path-aware diagnostics, and keeps the structured data for redisplay. - Trusted path —
Json.compile+Json.serialize/Json.deserialize: for payloads whose producer you trust, such as internal services, storage, caches, and queues. It enforces the wire shape and required fields, skips constraint checking, and runs about 6x faster with a fraction of the allocations (see the benchmarks).
Compile Once, Reuse Everywhere
open Reified
open Reified.Schema.Json
open Reified.SchemaDSL
type Address =
{ Street: string; City: string }
static member Schema(_: Address) : Schema<Address> =
schema<Address> {
field _.Street
field _.City
construct (fun street city -> { Street = street; City = city })
}
type Customer =
{ Name: string
Age: int
Address: Address }
let customerSchema =
schema<Customer> {
field _.Name
field _.Age
field _.Address
construct (fun name age address -> { Name = name; Age = age; Address = address })
}
let codec = Json.compile customerSchema // compile once, typically at startup
let json = Json.serialize codec { Name = "Ada"; Age = 36; Address = { Street = "12 Analytical Way"; City = "London" } }
// {"name":"Ada","age":36,"address":{"street":"12 Analytical Way","city":"London"}}
let customer = Json.deserialize codec json
ReifiedSchemaJsonReified.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.
08-schema_50-json-codecs.md_page.AddressStreet: stringstringAn abbreviation for the CLI type . Basic Types
City: stringSchema: Address -> Schema<Address>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<(Address -> string)> -> field<Address,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"
_arg2: Address_arg3: Addressconstruct: 'constructor -> SchemaCeBuilder.ConstructorStep<'model,'constructor>Closes a record schema with a total constructor.
street: stringcity: string08-schema_50-json-codecs.md_page.CustomerName: stringAge: intintAn abbreviation for the CLI type . Basic Types
Address: AddresscustomerSchema: Schema<Customer>``.ctor``: Quotations.Expr<(Customer -> string)> -> field<Customer,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: Customer``.ctor``: Quotations.Expr<(Customer -> int)> -> field<Customer,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: Customer``.ctor``: Quotations.Expr<(Customer -> Address)> -> field<Customer,Address>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"
_arg3: Customername: stringage: intaddress: Addresscodec: JsonCodec<Customer>Reified.Schema.Json.JsonFunctions for compiling and running JSON codecs over built model schemas.
compile: Schema<'model> -> JsonCodec<'model>Compiles a completed schema into a reusable JSON codec. Compile once per schema, typically at startup, and reuse the codec for every value. Constructor-last object schemas retain a typed record plan, including checked constructors. Constructor failures surface as during decoding. Thrown when is null. Thrown when is incomplete. open Reified.SchemaDSL open Reified.ConstraintDSL type Customer = { Name: string; Email: string } let customerSchema = schema<Customer> { field _.Name { constrain present } field _.Email { constraints [ present; email ] } construct (fun name email -> { Name = name; Email = email }) } let customer = { Name = "Ada"; Email = "ada@example.org" } let codec = Json.compile customerSchema let json = Json.serialize codec customer let roundTripped = Json.deserialize codec json
json: stringserialize: JsonCodec<'model> -> 'model -> stringSerializes a trusted model to a JSON string through a compiled codec. Thrown when is null.
customer: Customerdeserialize: JsonCodec<'model> -> string -> 'modelDeserializes a JSON string to a trusted model through a compiled codec. Thrown when or is null. Thrown when the JSON does not match the schema's wire shape.
Json.compile walks the typed record plan retained when the object shape closes and emits a direct plan: ordered field
descriptors, cached UTF-8 wire-name bytes, typed field decoders, and the original curried constructor applied without
boxing. Everything is compiler-directed: there is no runtime reflection at codec-compile time or per value, so the codec is AOT- and trimming-safe by construction.
Every Schema Shape Is Supported
Refined values encode as their raw representation and are reconstructed on decode; nested models, collections, and tagged unions follow the same wire shapes the input parser reads:
// A union field {"type":"card","value":{...}} round-trips through the same discriminator convention.
let orderCodec = Json.compile orderSchemaDecode Failures Carry Paths
Decoding trusted input can still meet malformed payloads. Failures raise JsonCodecException with a schema-relative
path, or use tryDeserialize for a Result:
match Json.tryDeserialize codec """{"name":"Ada","age":"not-a-number"}""" with
| Ok customer -> customer
| Error message -> failwith message // JSON decode failed at $.age: expected digit
Reified.Schema.Json.JsonFunctions for compiling and running JSON codecs over built model schemas.
tryDeserialize: JsonCodec<'model> -> string -> Result<'model,string>Deserializes a JSON string, returning decode failures as a rendered message instead of raising. Thrown when or is null.
codec: JsonCodec<Customer>OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
customer: CustomerErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
message: stringfailwith: string -> 'TThrow a exception. The exception message. Never returns. let failingFunction() = failwith "Oh no" // Throws an exception true // Never reaches this failingFunction() // Throws a System.Exception
The codec reports the first structural failure and stops. When you need every problem reported with redisplayable input — a form, a public API — that is boundary parsing's job:
// Boundary parsing: complete diagnostics for untrusted input.
let parsed = Schema.parse customerSchema (Data.ofJsonDocument document)Bytes In, Bytes Out
Json.serializeBytes and Json.deserializeBytes avoid the string conversion when the payload already lives as UTF-8
bytes, which is the faster path for network and storage boundaries:
let bytes = Json.serializeBytes codec customer
let roundTripped = Json.deserializeBytes codec bytes
bytes: byte arrayReified.Schema.Json.JsonFunctions for compiling and running JSON codecs over built model schemas.
serializeBytes: JsonCodec<'model> -> 'model -> byte arraySerializes a trusted model to UTF-8 JSON bytes through a compiled codec. Thrown when is null.
codec: JsonCodec<Customer>customer: CustomerroundTripped: CustomerdeserializeBytes: JsonCodec<'model> -> byte array -> 'modelDeserializes UTF-8 JSON bytes to a trusted model through a compiled codec. Thrown when or is null. Thrown when the JSON does not match the schema's wire shape.
What The Codec Does Not Do
- It does not run constraint metadata such as
maxLengthorbetween— those belong to boundary parsing and validation. A value that only ever passes through trusted systems does not pay for checks it already passed. - Checked constructors from
constructResultstill run, so intrinsic cross-field invariants hold on the trusted path; their errors surface asJsonCodecException.
From C#
Consume-don't-author: F# declares the schema, C# compiles the codec, parses, and reads diagnostics. Every Json.*
function takes plain positional arguments, so it calls as an ordinary static method with no FSharpFunc conversion:
using Reified.Schema;
using Reified.Schema.Json;
JsonCodec<Customer> codec = Json.compile(customerSchema);
string json = Json.serialize(codec, customer);
Customer roundTripped = Json.deserialize(codec, json);
// Failures raise JsonCodecException instead of a Result, or use tryDeserialize:
var attempt = Json.tryDeserialize(codec, json); // FSharpResult<Customer, string>
serializeToStream and deserializeStreamAsync (both async/Task-based) are also plain static calls, so they work
directly against HttpContext.Response.Body / Request.Body in an ASP.NET Core handler.
Next
- Serve the same declaration as a contract with
JsonSchema.generate. - See the two paths together in the runnable minimal API sample.