Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result
open Reified.Schema.JsonData
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"
]
ReifiedReified.DataDSLConcise opt-in syntax for literals, immutable edits, cases, and matching.
baseline: Datadata: DataField list -> DataBuilds an object from ordered field instructions. data [ "name" => "Ada"; "active" => true ] // Data.Object [ "name", Data.Text "Ada"; "active", Data.Bool true ]
(=>): string -> ^value -> DataFieldAssociates a field name with an exact value or recursive data pattern.
request: Data(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Reified.DataModulepatch: DataEdit list -> Data -> DataApplies edits atomically or raises DataPatchException. Data.data [ Data.assoc "name" "Ada" ] |> Data.patch [ DataEdit.replace "name" "Grace" ] // Data.data [ Data.assoc "name" "Grace" ]
replace: string -> ^a -> DataEditReplaces an existing value.
append: string -> ^a -> DataEditAppends an item to an existing list.
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\"] }"
Reified.DataModulerender: Data -> stringRenders structured data in a compact, human-readable form. Data.render (data [ "name" => "Ada"; "active" => true ]) // { name: "Ada", active: true }
request: DataNow 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
request: Data(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
matching: DataExpectation list -> Data -> unitChecks expectations or raises DataMatchException. open Reified.DataDSL let actual = data [ "user" => data [ "name" => "Ada" ] ] matching [ at "user.name" "Ada"; absent "error" ] actual // returns unit when both expectations hold; otherwise raises DataMatchException
at: string -> ^a -> DataExpectationRequires a path to contain an exact value or recursive pattern.
containing: DataField list -> DataPatternCreates a partial object pattern from required fields. Data.tryMatch [ at "" (containing [ "id" => 42 ]) ] (data [ "id" => 42; "extra" => true ]) // Ok ()
(=>): string -> ^value -> DataFieldAssociates a field name with an exact value or recursive data pattern.
containingItems: ^a list -> DataPatternMatches expected items as an unordered consumed subset. open Reified.DataDSL let actual = data [ "items" => [ "Ada"; "Grace"; "Mallory" ] ] Data.tryMatch [ at "items" (containingItems [ "Ada"; "Grace" ]) ] actual // Ok () when both values occur, in either order
absent: string -> DataExpectationRequires a path to be absent.
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" ]
ReifiedReified.DataDSLConcise opt-in syntax for literals, immutable edits, cases, and matching.
person: Datadata: DataField list -> DataBuilds an object from ordered field instructions. data [ "name" => "Ada"; "active" => true ] // Data.Object [ "name", Data.Text "Ada"; "active", Data.Bool true ]
(=>): string -> ^value -> DataFieldAssociates a field name with an exact value or recursive data pattern.
roles: string listUse ?=> 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 }"
nickname: string optionstringAn abbreviation for the CLI type . Basic Types
optionThe type of optional values. When used from other CLI languages the empty option is the null value. Use the constructors Some and None to create values of this type. Use the values in the Option module to manipulate values of this type, or pattern match against the values directly. 'None' values will appear as the value null to other CLI languages. Instance methods on this type will appear as static methods to other CLI languages due to the use of null as a value representation. Options
NoneThe representation of "No value"
account: Datadata: DataField list -> DataBuilds an object from ordered field instructions. data [ "name" => "Ada"; "active" => true ] // Data.Object [ "name", Data.Text "Ada"; "active", Data.Bool true ]
(?=>): string -> ^value option -> DataFieldAssociates a field name with an optional exact value, omitting None.
(=>): string -> ^value -> DataFieldAssociates a field name with an exact value or recursive data pattern.
nil: DataAn explicit structured null used by literals and edits.
Reified.DataModulerender: Data -> stringRenders structured data in a compact, human-readable form. Data.render (data [ "name" => "Ada"; "active" => true ]) // { name: "Ada", active: true }
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 ] ].