Tagged Union Wire Shape Reference
This page is for lookup once you already know the authored tagged-union API:
Schema.tagSchema.tagWithSchema.unionSchema.unionNamedSchema.inlineUnionSchema.inlineUnionNamedSchema.messageSchema.messageWithSchema.envelopeSchema.envelopeNamedSchema.inlineEnvelopeSchema.inlineEnvelopeNamedSchema.delay
It describes the exact wire shapes currently emitted by the built-in codecs.
Default field names
Schema.union uses these default wire field names:
- discriminator field:
case - payload field:
value
Example schema:
open CodecMapper
open CodecMapper.Schema
type Status =
| Pending
| Failed of string
let statusSchema =
union [
tag "pending" Pending ((=) Pending)
tagWith
"failed"
(function Failed message -> Some message | _ -> None)
Failed
string
]
JSON shape
Payload-free cases encode as an object with only the discriminator:
|
Payload cases encode as an object with both fields:
|
XML shape
XML uses the schema-derived root element name, then nested discriminator and payload elements:
|
|
YAML shape
YAML projects the same JSON structure:
|
|
KeyValue shape
KeyValue flattens the same contract into dotted paths:
|
|
Nested payloads keep extending the path:
|
Custom field names with unionNamed
Schema.unionNamed discriminatorName valueName changes the wire field names without changing the authored case names.
Example:
let statusSchema =
unionNamed "kind" "details" [
tag "pending" Pending ((=) Pending)
tagWith
"failed"
(function Failed message -> Some message | _ -> None)
Failed
string
]
That changes the wire shape like this.
JSON:
|
XML:
|
YAML:
|
KeyValue:
|
Inline payload fields with inlineUnion
Schema.inlineUnion keeps the discriminator field, but merges payload members
into the same object level instead of nesting them under a separate payload
field.
Example:
type CreatedData = { Id: int; Name: string }
type Event =
| Ping
| Created of CreatedData
let createdDataSchema =
define<CreatedData>
|> construct (fun id name -> { Id = id; Name = name })
|> field "id" _.Id
|> field "name" _.Name
|> build
let eventSchema =
inlineUnion [
tag "ping" Ping ((=) Ping)
tagWith
"created"
(function Created payload -> Some payload | _ -> None)
Created
createdDataSchema
]
JSON:
|
XML:
|
YAML:
|
KeyValue:
|
Inline payload schemas must be object-shaped so the payload can contribute named members cleanly across all formats. Record schemas are the intended fit here.
Custom discriminator names with inlineUnionNamed
Schema.inlineUnionNamed discriminatorName changes only the discriminator
field name for the inline shape.
JSON:
|
Message and envelope helpers
For message and event contracts, the helper names can read more directly than the generic tagged-union names:
messageis the same authored tag shape astagmessageWithis the same authored tag shape astagWithenvelopeuses"type"/"data"field names by defaultinlineEnvelopeuses"type"and inlines payload members next to it
Example envelope:
let eventSchema =
envelope [
message "ping" Ping ((=) Ping)
messageWith
"created"
(function Created payload -> Some payload | _ -> None)
Created
createdDataSchema
]
JSON:
|
Inline envelope JSON:
|
Recursive unions with delay
Schema.delay lets a union point back to itself:
type RecursiveNode =
| Leaf of string
| Branch of RecursiveNode
let rec nodeSchema : Schema<RecursiveNode> =
delay (fun () ->
union [
tagWith
"leaf"
(function Leaf value -> Some value | _ -> None)
Leaf
string
tagWith
"branch"
(function Branch value -> Some value | _ -> None)
Branch
nodeSchema
])
That recursive authored contract currently compiles for:
- JSON
- XML
- YAML
- KeyValue
JsonSchema.generate also exports it as a structural schema using local $defs / $ref.
Decode failure behavior
The codecs currently reject:
- unknown case names
- missing payload fields for payload cases
- stray payload keys for payload-free KeyValue cases
- unknown inline case names
- missing inline payload fields for payload tags
- stray inline payload fields for payload-free tags
For KeyValue specifically, the payload-free case check matters because extra flattened keys would otherwise be easy to miss.
For readable, executable examples of malformed payloads and the expected error messages across JSON, XML, YAML, and KeyValue, see tests/CodecMapper.Tests/TaggedUnionErrorTests.fs.
val string: value: 'T -> string
--------------------
type string = System.String
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
module Event from Microsoft.FSharp.Control
--------------------
type Event<'T> = new: unit -> Event<'T> member Trigger: arg: 'T -> unit member Publish: IEvent<'T> with get
--------------------
type Event<'Delegate,'Args (requires delegate and 'Delegate :> Delegate and reference type)> = new: unit -> Event<'Delegate,'Args> member Trigger: sender: objnull * args: 'Args -> unit member Publish: IEvent<'Delegate,'Args> with get
--------------------
new: unit -> Event<'T>
--------------------
new: unit -> Event<'Delegate,'Args>
CodecMapper