Header menu logo CodecMapper

Getting Started with CodecMapper

This tutorial teaches the main authored-schema path first.

The goal is simple:

  1. define one schema
  2. compile it once
  3. serialize and deserialize with the same contract

Leave the C# bridge, JSON Schema import, and other side paths until this flow feels natural.

The smallest complete example

open CodecMapper
open CodecMapper.Schema

type Person = { Id: int; Name: string }
let makePerson id name = { Id = id; Name = name }

let personSchema =
    define<Person>
    |> construct makePerson
    |> field "id" _.Id
    |> field "name" _.Name
    |> build

let codec = Json.compile personSchema

let person = { Id = 1; Name = "Ada" }
let json = Json.serialize codec person
let decoded = Json.deserialize codec json

That is the normal shape of the library:

How to read the schema

Read the schema pipeline from top to bottom:

The important idea is that the schema is the contract itself, not a hint to a serializer.

What compilation means

The schema definition is still just data about the contract. Json.compile turns that definition into a reusable codec:

let codec = Json.compile personSchema

That 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 =
    define<Person>
    |> construct makePerson
    |> field "id" _.Id
    |> field "name" _.Name
    |> Json.buildAndCompile

Use that helper for small inline examples. Keep Json.compile personSchema when the schema has a name, is reused, or is referenced by other schemas.

The next step: nested data

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 =
    define<Address>
    |> construct makeAddress
    |> field "street" _.Street
    |> field "city" _.City
    |> build

let personSchema =
    define<Person>
    |> construct makePerson
    |> field "id" _.Id
    |> field "name" _.Name
    |> fieldWith "home" _.Home addressSchema
    |> build

fieldWith marks an explicit schema boundary for the child value.

The next step: stronger domain types

If the wire value is simple but the in-memory value should be validated, refine the schema with 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 =
    int
    |> tryMap UserId.create UserId.value

That keeps the wire contract simple while making the domain type stricter.

Where to go next

Take the next pages in this order:

  1. How To Model A Basic Record
  2. How To Model A Nested Record
  3. How To Model A Validated Wrapper
  4. How To Model A Versioned Contract

Once the authored-schema path is clear:

type Person = { Id: int Name: string }
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
val makePerson: id: int -> name: string -> Person
val id: int
val name: string
val personSchema: obj
val codec: obj
val person: Person
val json: obj
val decoded: obj
type Address = { Street: string City: string }
val makeAddress: street: string -> city: string -> Address
val street: string
val city: string
val makePerson: id: int -> name: string -> home: 'a -> Person
val home: 'a
val addressSchema: obj
Multiple items
union case UserId.UserId: int -> UserId

--------------------
type UserId = | UserId of int
type UserId = | UserId of int
val create: value: int -> Result<UserId,string>
val value: int
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
val value: UserId -> int
val userIdSchema: obj
Multiple items
union case UserId.UserId: int -> UserId

--------------------
module UserId from GETTINGSTARTED

--------------------
type UserId = | UserId of int

Type something to start searching.