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

Separate wire and domain models

Wire formats and domain models change for different reasons. A stored payload may need old field names and versions. Business code needs types that make current invariants easy to rely on.

A wire model represents serialized data such as JSON, a message, or a stored document. A domain model represents the business concept used by application logic. An invariant is a rule every valid domain value must satisfy.

Keep those jobs in separate types. This page assumes you have already chosen generation over a hand-written schema; see Schema or Generated DTO for that decision and for keeping the wire record free of constraint attributes so validation stays in the domain mapping below, not duplicated on the DTO.

Define a permissive wire record

[<DeriveSchema>] marks a public, namespace-level record whose Reified schema is generated from its source declaration. The schema describes how input fields parse and which constraints apply.

Fragment
namespace MyApp.Contracts

open Reified.DerivedSchema

[<DeriveSchema>]
type BookingWire =
    { Start: DateTimeOffset
      End: DateTimeOffset
      [<SchemaName "customer_note">]
      Note: string option }

Wire records remain public on purpose. They describe data at a boundary; they do not claim that business invariants hold.

Run generation as part of the build

Reference the build package alongside the ordinary Reified.Schema package the generated code calls into. [<DeriveSchema>] records are discovered in the project's ordinary F# compile files; generated code is placed under obj and inserted in the correct compile order automatically.

<ItemGroup>
  <PackageReference
      Include="Reified.SchemaGen.MSBuild"
      Version="..."
      PrivateAssets="all" />
</ItemGroup>

Set ReifiedSchemaGeneratedFiles to CheckedIn when the repository deliberately commits generated siblings. Do not add those .g.fs files to <Compile>; the target inserts them after their declaration files.

The generated module contains schema, parse, and validate. Stale generated code is replaced during the normal build; no separate generator command is needed.

Admit the wire value into the domain

Map the public wire record into a private aggregate through its real constructor. The rest of this page continues the same BookingWire/Booking illustration (BookingWire generated above, Booking the private aggregate from Build A Private Aggregate); no later fragment redeclares either type.

Fragment
let toDomain (wire: BookingWire) =
    Booking.create
        { Start = wire.Start
          End = wire.End }

The complete boundary is explicit:

Fragment
let parseBooking raw : Result<Booking, string> =
    BookingWire.parse raw
    |> Result.mapError SchemaErrors.toString
    |> Result.bind (fun wire ->
        toDomain wire
        |> Result.mapError (function
            | BookingError.EndBeforeStart -> "End must not be before start."))

After this function succeeds, pass Booking into application code. Do not pass BookingWire inward and rely on every caller to remember which checks ran.

Map back explicitly

Persistence and response code use a named domain-to-wire function.

Fragment
let fromDomain booking : BookingWire =
    { Start = Booking.start booking
      End = Booking.finish booking
      Note = None }

This mapping is the review point for representation choices such as omitted fields, defaults, and renamed properties.

Add versions on the wire side

Keep old wire records and typed migrations in the Contracts project. Migrate to the current wire type, then perform the single current wire-to-domain mapping.

stored input
  -> parse frozen wire version
  -> typed migrations
  -> current wire value
  -> Booking.create
  -> domain Booking

The generator emits the typed version-chain builder. Adding a version changes that builder, so contract construction does not compile until the new migration is supplied.

See Generated version series for the version-series attribute vocabulary and migration setup, and Build Generation for running generation in your build.