This tutorial teaches the main schema-authoring path first.
The goal is simple:
- define one schema
- compile it once
- serialize and deserialize with the same schema
Leave the C# bridge, JSON Schema import, and other side paths until this flow feels natural.
open CodecMapper
type Person = { Id: int; Name: string }
let makePerson id name = { Id = id; Name = name }
let personSchema =
Schema.record makePerson
|> Schema.field "id" _.Id
|> Schema.field "name" _.Name
|> Schema.build
let codec = Json.compile personSchema
let person = { Id = 1; Name = "Ada" }
let json = Json.serialize codec person
let decoded = Json.deserialize codec jsonThat is the normal shape of the library:
- author a schema
- compile it into a codec
- reuse that codec for both directions
Read the builder pipeline from top to bottom:
Schema.record makePersonsays which value the schema describes and how decode rebuilds itSchema.field "id" _.Idmaps the wire field"id"to the record fieldIdSchema.buildfinishes the schema
The important idea is that the schema is explicit code, not a hint to a serializer.
The authored schema is still just data about the wire shape. Json.compile turns that definition into a reusable codec:
let codec = Json.compile personSchemaThat explicit step matters because CodecMapper is designed for reuse. You compile once, then serialize and deserialize many values with the same codec.
If the schema is only being authored inline at the end of a short example, Json.buildAndCompile is a convenience:
let codec =
Schema.record makePerson
|> Schema.field "id" _.Id
|> Schema.field "name" _.Name
|> Json.buildAndCompileUse that helper for small inline examples. Keep Json.compile personSchema when the schema has a name, is reused, or is referenced by other schemas.
A child record usually gets its own schema:
type Address = { Street: string; City: string }
let makeAddress street city = { Street = street; City = city }
type Person = { Id: int; Name: string; Home: Address }
let makePerson id name home = { Id = id; Name = name; Home = home }
let addressSchema =
Schema.record makeAddress
|> Schema.field "street" _.Street
|> Schema.field "city" _.City
|> Schema.build
let personSchema =
Schema.record makePerson
|> Schema.field "id" _.Id
|> Schema.field "name" _.Name
|> Schema.fieldWith "home" _.Home addressSchema
|> Schema.buildSchema.fieldWith marks an explicit schema boundary for the child value.
If the wire value is simple but the in-memory value should be validated, refine the schema with Schema.tryMap:
type UserId = UserId of int
module UserId =
let create value =
if value > 0 then Ok(UserId value)
else Error "UserId must be positive"
let value (UserId value) = value
let userIdSchema =
Schema.int
|> Schema.tryMap UserId.create UserId.valueThat keeps the wire schema simple while making the domain type stricter.
Take the next pages in this order:
- How To Model A Basic Record
- How To Model A Nested Record
- How To Model A Validated Wrapper
- How To Model A Versioned Schema
- How To Model A Recursive Tagged Union
Once the schema-authoring path is clear:
- use How To Import Existing C# Contracts for bridge or C# facade work
- use How To Export JSON Schema for outward schema documents
- use JSON Schema in CodecMapper when you need the design reasoning