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

Data

Reified.Data makes structured data concise to build and change. Its objects, lists, text, numbers, Booleans, and null map directly to JSON, but the same model works well for test fixtures, configuration, command-line input, form values, events, and other tree-shaped data.

Start with one readable value, derive related cases without copying it, and test either the complete result or only the fields that matter.

This is useful when tests otherwise accumulate large JSON strings, nested constructors, or near-identical fixtures. The data stays structured, edits identify exactly what changes, and failures point to the path that differs.

Install

Install the package with:

dotnet add package Reified.Data

Build, change, and check one value

open Reified
open Reified.DataDSL

let baseline =
    data [
        "name" => "Ada"
        "plan" => "free"
        "address" => [
            "city" => "Adelaide"
            "postcode" => 5000
        ]
        "roles" => [ "author" ]
    ]

let request =
    baseline
    |> Data.patch [
        replace "plan" "pro"
        append "roles" "admin"
    ]

request contains the changed plan and the additional role. baseline is unchanged.

Data.render request
// => "{ name: \"Ada\", plan: \"pro\", address: { city: \"Adelaide\", postcode: 5000 }, roles: [\"author\", \"admin\"] }"

Now check only the parts of the result that matter:

request
|> matching [
    at "name" "Ada"
    at "address" (containing [ "postcode" => 5000 ])
    at "roles" (containingItems [ "admin" ])
    absent "error"
]
// succeeds

If an expectation fails, matching raises DataMatchException with the mismatched path and values. Extra fields and the additional author role are allowed because these patterns check only the values named here.

Basic syntax

Use data to build a Data value. Lists represent both objects and lists. A list containing name => value fields is an object; a list containing ordinary values is a list.

open Reified
open Reified.DataDSL

let person = data [ "name" => "Ada"; "active" => true ]
let roles = [ "author"; "admin" ]

Use ?=> for an optional field. Some value includes the field; None leaves the field out altogether. Use name => nil when the field must be present with a null value.

let nickname : string option = None
let account = data [ "nickname" ?=> nickname; "deletedAt" => nil ]

Data.render account
// => "{ deletedAt: null }"

Build data with ordinary F# control flow

data takes an F# list of fields, so the whole list-expression vocabulary is available inside it. Include another object's fields with yield!, add a field only under a condition with if, and generate fields from a sequence with for. No builder, no intermediate dictionary, no post-hoc filtering of nulls.

let event =
    data [
        "kind" => "example"
        "customerId" => customerId
        yield! fields common

        if includeDebug then
            "debug" => true

        for name in names do
            $"user-{name}" => name
    ]

With common = data [ "tenant" => "acme"; "region" => "au" ], customerId = "c-1", includeDebug = true, and names = [ "ada"; "grace" ]:

Data.render event
// => "{ kind: \"example\", customerId: \"c-1\", tenant: \"acme\", region: \"au\", debug: true, user-ada: \"ada\", user-grace: \"grace\" }"

Fields appear in the order they are yielded, so a conditional or generated field lands exactly where it is written. The same works for lists: data [ "ids" => [ for id in ids -> id * 10 ] ].

Learn and solve tasks