Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result
open Reified.Schema.JsonAdvanced Union Handling
Use the default model for new contracts. Use Schema.unionWith when stored documents, clients, or another serializer
already require a different union shape.
The chosen format lives in the schema. Reified uses it consistently for parsing, JSON output, JSON Schema, generated test data, and inspection.
Ready-made compatibility formats
| Format | Use | JSON |
|---|---|---|
| Internal tag and named fields | UnionRepresentations.recommended |
{ "type": "move", "x": 1, "y": 2 } |
| FSharp.SystemTextJson default | UnionRepresentations.fsharpSystemTextJsonDefault |
{ "Case": "Move", "Fields": [1, 2] } |
| FSharp.SystemTextJson external, named, and unwrapped | UnionRepresentations.fsharpSystemTextJsonExternalNamed |
"stop", { "rename": "Ada" }, { "resize": { "width": 1, "height": 2 } } |
| Adjacent tag and named payload | UnionRepresentations.reifiedAdjacent |
{ "type": "move", "value": { "x": 1, "y": 2 } } |
| External tag and named payload | UnionRepresentations.compactExternal |
"stop", { "move": { "x": 1, "y": 2 } } |
Pass one of these values with the same case catalogue used by Schema.union:
let commandSchema =
Schema.unionWith
UnionRepresentations.fsharpSystemTextJsonDefault
commandCasesThat profile reads and writes FSharp.SystemTextJson's default Case/Fields format:
{ "Case": "Stop" }
{ "Case": "SetVolume", "Fields": [12.5] }
{ "Case": "Move", "Fields": [10, 20] }
Generate a compatibility format
Set the representation and payload style on [<DeriveUnion>]:
[<DeriveUnion(
"Case",
Representation = UnionRepresentationKind.Adjacent,
PayloadField = "Fields",
PayloadStyle = UnionPayloadStyleKind.Positional)>]
type ExistingCommand =
| [<SchemaName "Stop">] Stop
| [<SchemaName "SetVolume">] SetVolume of amount: decimal
| [<SchemaName "Move">] Move of x: int * y: intThe [<SchemaName>] attributes preserve PascalCase case tags. The named F# fields give schemagen enough information
to construct the cases, but their names do not appear in the positional arrays.
The external named/unwrapped profile writes empty cases as strings, one-field cases as scalars, and multi-field cases as named objects:
[<DeriveUnion(
Representation = UnionRepresentationKind.External,
PayloadStyle = UnionPayloadStyleKind.NamedWithUnwrappedSingle,
UnwrapFieldless = true)>]
type ExistingExternalCommand =
| Stop
| Rename of name: string
| Resize of width: int * height: int"stop"
{ "rename": "Ada" }
{ "resize": { "width": 10, "height": 20 } }
Keep existing field names
If an established named-field contract uses item, item1, or item2, retain meaningful F# names and override the
wire names explicitly:
[<DeriveUnion>]
type Command =
| SetVolume of [<SchemaName "item">] amount: decimal
| Move of [<SchemaName "item1">] x: int * [<SchemaName "item2">] y: int{ "type": "setVolume", "item": 12.5 }
{ "type": "move", "item1": 10, "item2": 20 }
Application code still uses amount, x, and y. Only the JSON keys retain the compatibility names.
In a handwritten payload schema, call fieldAs once per field. Its first argument is the JSON key; its second argument
reads the meaningful F# field:
let moveFields =
schema<{| x: int; y: int |}> {
fieldAs "item1" _.x
fieldAs "item2" _.y
construct (fun x y -> {| x = x; y = y |})
}
let moveCase =
UnionCase.fields
"move"
(fun fields -> Move(fields.x, fields.y))
(function
| Move(x, y) -> Some {| x = x; y = y |}
| _ -> None)
moveFieldsPut several fields in one array
Renaming individual fields cannot produce { "type": "move", "items": [10, 20] }. That JSON uses adjacent tagging
with a positional payload:
let commandSchema =
Schema.unionWith
(UnionRepresentation.Adjacent(
"type",
"items",
UnionPayloadStyle.Positional
))
commandCases{ "type": "setVolume", "items": [12.5] }
{ "type": "move", "items": [10, 20] }
With FSharp.SystemTextJson, select adjacent tagging, use "type" as the tag name, use "items" as the union-fields
name, and leave named fields disabled.
Build a representation directly
For a format not covered by a ready-made value, construct one of:
UnionRepresentation.Internal("kind")
UnionRepresentation.Adjacent("kind", "fields", UnionPayloadStyle.Named)
UnionRepresentation.External(UnionPayloadStyle.Named, true)
Reified.UnionRepresentationA complete, inspectable union wire representation.
InternalAdjacentReified.UnionPayloadStyleControls how a case's authored fields are represented inside adjacent or external tagging.
NamedExternalAdjacent and external formats accept these payload styles:
| Style | JSON for a two-field case | JSON for a one-field case |
|---|---|---|
UnionPayloadStyle.Named |
{ "x": 10, "y": 20 } |
{ "amount": 12.5 } |
UnionPayloadStyle.Positional |
[10, 20] |
[12.5] |
UnionPayloadStyle.UnwrappedSingle |
invalid for two fields | 12.5 |
UnionPayloadStyle.NamedWithUnwrappedSingle |
{ "x": 10, "y": 20 } |
12.5 |
UnionPayloadStyle.PositionalWithUnwrappedSingle |
[10, 20] |
12.5 |
UnwrappedSingle requires every payload case to contain exactly one field. The adaptive styles unwrap only one-field
cases and retain named objects or positional arrays for larger cases.
Match Reified's default in FSharp.SystemTextJson
Use this configuration when FSharp.SystemTextJson and Reified must write the same default JSON:
let fsharpOptions =
JsonFSharpOptions
.Default()
.WithUnionInternalTag()
.WithUnionTagName("type")
.WithUnionNamedFields()
.WithUnionTagNamingPolicy(JsonNamingPolicy.CamelCase)
.WithUnionFieldNamingPolicy(JsonNamingPolicy.CamelCase)
.WithUnionUnwrapSingleCaseUnions(true)
.WithSkippableOptionFields(SkippableOptionFields.FromJsonSerializerOptions)
let jsonOptions =
JsonSerializerOptions(PropertyNamingPolicy = JsonNamingPolicy.CamelCase)Apply WithUnionUnwrapFieldlessTags() only to an all-fieldless union through a per-type converter attribute. If it is
global, a mixed union can write its empty case as "stop" while writing its other cases as tagged objects. Reified
uses a string only when every case is fieldless.
Optional record fields omit None by default. Add Schema.mustSupply when the key must be present; None then writes
as JSON null.
JSON Schema and inspection
JSON Schema output matches the selected JSON:
- internal unions become
oneOfobject branches with a required constant tag; - positional payloads become fixed arrays using
prefixItems,minItems, andmaxItems; - external unions become single-property objects, plus strings when fieldless tags are unwrapped;
- enums become string
enumvalues; - transparent values use the underlying value's schema.
Inspect returns the chosen UnionRepresentation and each case's empty, value, or named-fields form. Tools built on
Reified therefore see the same format that the parser and JSON codec use.