Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result
open Reified.Schema.JsonDeclare, render, and edit data
Open Reified.Data for Data. Open Reified.DataDSL for the concise operators and functions used on this page.
open Reified
open Reified.DataDSL
ReifiedReified.DataDSLConcise opt-in syntax for literals, immutable edits, cases, and matching.
Declare objects and lists
data builds an object from field declarations. => adds a field. A nested field list becomes another object, while
a list of ordinary values becomes Data.List.
let customer =
data [
"name" => "Ada"
"active" => true
"address" => [
"city" => "Adelaide"
"postcode" => 5000
]
"roles" => [ "author"; "admin" ]
]
Data.render customer
// => "{ name: \"Ada\", active: true, address: { city: \"Adelaide\", postcode: 5000 }, roles: [\"author\", \"admin\"] }"
customer: 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.
Reified.DataModulerender: Data -> stringRenders structured data in a compact, human-readable form. Data.render (data [ "name" => "Ada"; "active" => true ]) // { name: "Ada", active: true }
Literal fields accept Data, string, bool, int, int64, decimal, finite float, Guid, DateTimeOffset,
DateOnly on .NET 8+, supported lists, and nested field lists.
Omit a field or write null
?=> adds a Some value and omits None. nil writes a present JSON null.
let value =
data [
"nickname" ?=> (None: string option)
"deletedAt" => nil
]
Data.render value
// => "{ deletedAt: null }"
value: 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.
NoneThe representation of "No value"
stringAn 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
(=>): 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 }
Reuse object fields
fields returns the fields of an existing object so they can be included in a new declaration.
let address = data [ "city" => "Adelaide"; "postcode" => 5000 ]
let customerWithAddress = data [ yield! fields address; "name" => "Ada" ]
Data.render customerWithAddress
// => "{ city: \"Adelaide\", postcode: 5000, name: \"Ada\" }"
address: 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.
customerWithAddress: Datafields: Data -> DataField listReturns exact field instructions for spreading an existing object literal.
Reified.DataModulerender: Data -> stringRenders structured data in a compact, human-readable form. Data.render (data [ "name" => "Ada"; "active" => true ]) // { name: "Ada", active: true }
Build fields with control flow
The argument to data is an ordinary F# list of DataField, so every list-expression form works inside it. Mix
literal fields with yield!, if, for, match, and let bindings in one declaration.
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\" }"Points worth knowing:
- Fields keep the order they are yielded. A conditional or generated field appears where it is written, not appended at the end.
yield!splices aDataField list.fieldsproduces one from an existing object;Data.fieldsis the explicit name. Splicing raises if the value is not an object.- A bare
fieldin the list is an implicityield. F# permits mixing implicit yields withif,for, andyield!in the same list, so noyieldkeyword is needed on the plain lines. - An
ifwithoutelsecontributes nothing when the condition is false. Use?=>instead when the choice isSome/Noneon a single field, andif/forwhen the shape of the object varies. - The same forms build lists:
data [ "ids" => [ for id in ids -> id * 10 ] ]renders{ ids: [10, 20, 30] }.
Control flow decides which fields exist. Data.patch changes fields that already exist. Prefer control flow when
building a value from inputs, and patching when deriving a variation from a value you already have.
Render for people
Data.render returns a compact display with unquoted ordinary field names and quoted text. Data.renderIndented
returns the same notation with line breaks and indentation. Both preserve object field order, duplicate fields, and
number tokens. Use Data.Json.render when the result must be JSON.
Data.renderIndented (data [ "name" => "Ada" ])
// => "{\n name: \"Ada\"\n}"Edit without changing the original
Use a direct Data operation for one change. It returns the changed tree and leaves the original unchanged.
let renamed = customer |> Data.replace "name" "Grace"
Data.lookupPath "name" renamed
// => Data.Text "Grace"
Data.lookupPath "name" customer
// => Data.Text "Ada"Every edit is available directly:
| Direct operation | Result |
|---|---|
Data.set path value input |
Replace a value, or add a missing final object field. |
Data.replace path value input |
Replace an existing value; fail if it is missing. |
Data.remove path input |
Remove an existing field or list item. |
Data.append path value input |
Add an item to the end of a list. |
Data.prepend path value input |
Add an item to the start of a list. |
Data.insert path index value input |
Add an item at a list index. |
Data.rename path newName input |
Rename an object field without moving it. |
Data.update path function input |
Replace a value with the function result. |
customer
|> Data.set "plan" "pro"
|> Data.append "roles" "admin"
|> Data.rename "address.city" "suburb"
|> Data.remove "active"
customer: 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.DataModuleset: string -> ^a -> Data -> DataReplaces one value, or adds a missing final object field, and returns the changed tree. data [ "name" => "Ada" ] |> Data.set "active" true // data [ "name" => "Ada"; "active" => true ]
append: string -> ^a -> Data -> DataAppends one item to an existing list and returns the changed tree. data [ "roles" => [ "author" ] ] |> Data.append "roles" "admin" // data [ "roles" => [ "author"; "admin" ] ]
rename: string -> string -> Data -> DataRenames one existing object field without moving it and returns the changed tree. data [ "name" => "Ada" ] |> Data.rename "name" "displayName" // data [ "displayName" => "Ada" ]
remove: string -> Data -> DataRemoves one existing field or list item and returns the changed tree. data [ "name" => "Ada"; "active" => true ] |> Data.remove "active" // data [ "name" => "Ada" ]
Data.replace, remove, append, prepend, insert, rename, and update require their target to exist.
Data.set may add its final object field, but its parent must exist. Shape and path failures raise
DataPatchException.
Apply several edits atomically
Data.patch applies a list of edits in order. It returns a new value only when every edit succeeds.
let changed =
customer
|> Data.patch [
replace "name" "Grace"
set "plan" "pro"
append "roles" "admin"
]
changed: Datacustomer: 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.
set: string -> ^a -> DataEditReplaces a value or adds a missing final object field.
append: string -> ^a -> DataEditAppends an item to an existing list.
Inside Data.patch, use the unqualified edit constructors from Reified.DataDSL:
| Operation | Result |
|---|---|
set path value |
Replace a value, or add a missing final object field. |
replace path value |
Replace an existing value; fail if it is missing. |
remove path |
Remove an existing field or list item. |
append path value |
Add an item to the end of a list. |
prepend path value |
Add an item to the start of a list. |
insert path index value |
Add an item at a list index. |
rename path newName |
Rename an object field without moving it. |
update path function |
Replace a value with the function result. |
Every operation except a missing final object field handled by set requires its target to exist. If an edit fails,
none of the edits are returned as a partial result.
Data.patch raises DataPatchException. Data.tryPatch returns the failure instead:
Data.tryPatch [ append "name" "Grace" ] customer
// => Error [
// { EditIndex = 0
// Path = "name"
// Message = "Expected a list but found text." }
// ]
Reified.DataModuletryPatch: DataEdit list -> Data -> Result<Data,DataPatchFailure list>Applies immutable edits atomically in declaration order. Data.tryPatch [ replace "name" "Grace" ] (data [ "name" => "Ada" ]) // Ok (data [ "name" => "Grace" ])
append: string -> ^a -> DataEditAppends an item to an existing list.
customer: Data