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

Data

Maturity: Incubating

Reified.Data builds and changes structured data. Its objects, lists, text, numbers, Booleans, and null map directly to JSON, and also work for test fixtures, configuration, command-line input, form values, events, and other tree-shaped data.

Build one value, derive related cases without copying it, and test the complete result or selected fields. This avoids large JSON strings, nested constructors, and near-identical fixtures. Failures identify the path that differs.

Install

Install the package with:

dotnet add package Reified.Data

Build, change, and check one value

Verified
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.

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

Check selected parts of the result:

Verified
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. Fields declared with name => value make an object; ordinary values in the same list position make a Data.List instead.

Verified
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.

Verified
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. yield! splices another object's fields, if adds a field under a condition, and for generates fields from a sequence:

Verified
let common = data [ "tenant" => "acme"; "region" => "au" ]
let names = [ "ada"; "grace" ]

let event =
    data [
        "kind" => "example"
        yield! fields common
        for name in names do
            $"user-{name}" => name
    ]

Fields keep yield order, including conditional and generated fields; the same forms build lists. The complete example, with conditional fields and rendered output, is in Declare, render, and edit data.

Learn and solve tasks