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.
Schema.union uses these default wire field names:
- discriminator field:
case - payload field:
value
Example schema:
open CodecMapper
type Status =
| Pending
| Failed of string
let statusSchema =
Schema.union [
Schema.tag "pending" Pending ((=) Pending)
Schema.tagWith
"failed"
(function Failed message -> Some message | _ -> None)
Failed
Schema.string
]Payload-free cases encode as an object with only the discriminator:
{"case":"pending"}Payload cases encode as an object with both fields:
{"case":"failed","value":"boom"}XML uses the schema-derived root element name, then nested discriminator and payload elements:
<status><case>pending</case></status><status><case>failed</case><value>boom</value></status>YAML projects the same JSON structure:
case: pendingcase: failed
value: boomKeyValue flattens the same schema into dotted paths:
case=pending
case=failed
value=boom
Nested payloads keep extending the path:
case=branch
value.case=branch
value.value.case=leaf
value.value.value=ok
Schema.unionNamed discriminatorName valueName changes the wire field names without changing the authored case names.
Example:
let statusSchema =
Schema.unionNamed "kind" "details" [
Schema.tag "pending" Pending ((=) Pending)
Schema.tagWith
"failed"
(function Failed message -> Some message | _ -> None)
Failed
Schema.string
]That changes the wire shape like this.
JSON:
{"kind":"failed","details":"boom"}XML:
<status><kind>failed</kind><details>boom</details></status>YAML:
kind: failed
details: boomKeyValue:
kind=failed
details=boom
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 makeCreatedData id name = { Id = id; Name = name }
let createdDataSchema =
Schema.record makeCreatedData
|> Schema.field "id" _.Id
|> Schema.field "name" _.Name
|> Schema.build
let eventSchema =
Schema.inlineUnion [
Schema.tag "ping" Ping ((=) Ping)
Schema.tagWith
"created"
(function Created payload -> Some payload | _ -> None)
Created
createdDataSchema
]JSON:
{"case":"created","id":7,"name":"Ada"}XML:
<event><case>created</case><id>7</id><name>Ada</name></event>YAML:
case: created
id: 7
name: AdaKeyValue:
case=created
id=7
name=Ada
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.
Schema.inlineUnionNamed discriminatorName changes only the discriminator
field name for the inline shape.
JSON:
{"kind":"created","id":7,"name":"Ada"}For message and event schemas, 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 =
Schema.envelope [
Schema.message "ping" Ping ((=) Ping)
Schema.messageWith
"created"
(function Created payload -> Some payload | _ -> None)
Created
createdDataSchema
]JSON:
{"type":"created","data":{"id":7,"name":"Ada"}}Inline envelope JSON:
{"type":"created","id":7,"name":"Ada"}Schema.delay lets a union point back to itself:
type RecursiveNode =
| Leaf of string
| Branch of RecursiveNode
let rec nodeSchema : Schema<RecursiveNode> =
Schema.delay (fun () ->
Schema.union [
Schema.tagWith
"leaf"
(function Leaf value -> Some value | _ -> None)
Leaf
Schema.string
Schema.tagWith
"branch"
(function Branch value -> Some value | _ -> None)
Branch
nodeSchema
])That recursive authored schema currently compiles for:
- JSON
- XML
- YAML
- KeyValue
JsonSchema.generate also exports it as a structural schema using local $defs / $ref.
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.