Specification scope: Core only.
Without an explicit tag, a consumer may have to inspect fields or try union branches in order. That becomes ambiguous when two variants share the same shape.
JSON Structure’s tagged choice uses a one-property JSON object. The property
name selects the variant, and its value contains the variant data. A consumer
reads the tag before validating the value.
A payment method choice
The payment-method schema declares card and bank-transfer variants:
{
"$schema": "https://json-structure.org/meta/core/v0/#",
"$id": "https://example.com/schemas/payment-method",
"name": "PaymentMethod",
"type": "choice",
"choices": {
"card": {
"type": { "$ref": "#/definitions/CardPayment" }
},
"bankTransfer": {
"type": { "$ref": "#/definitions/BankTransferPayment" }
}
},
"definitions": {
"CardPayment": {
"name": "CardPayment",
"type": "object",
"properties": {
"network": { "type": "string" },
"lastFour": { "type": "string" },
"token": { "type": "string" }
},
"required": ["network", "lastFour", "token"],
"additionalProperties": false
},
"BankTransferPayment": {
"name": "BankTransferPayment",
"type": "object",
"properties": {
"accountHolder": { "type": "string" },
"iban": { "type": "string" }
},
"required": ["accountHolder", "iban"],
"additionalProperties": false
}
}
}
A card instance is wrapped by the card tag:
{
"card": {
"network": "visa",
"lastFour": "4242",
"token": "tok_7d91"
}
}
A bank transfer uses the other property name:
{
"bankTransfer": {
"accountHolder": "Ada Lovelace",
"iban": "DE89370400440532013000"
}
}
The keys of choices are the selectors. There is no separate type, kind, or
method field in a tagged choice. The wrapper object must have one property,
and that property’s name selects the corresponding schema.
An object containing both card and bankTransfer is not two choices at once;
it is invalid. So is {"cash": {}}, because cash is not a declared choice.
The consumer dispatches on one key and validates one value.
Tags survive schema evolution
Branch selection depends on the tag, not on the properties inside the selected value.
Imagine that bank transfers later gain a token issued by a payment provider.
A shape-inferred union can no longer distinguish the branches by the presence
of token. bankTransfer still selects BankTransferPayment, even if the two
object types eventually share every property name.
Renaming a choice key is therefore a wire-format change, not a cosmetic schema edit. Generated enum cases, serializers, and stored JSON all observe it. Variant type names and choice keys may be similar, but they serve different roles: the reference names a reusable schema; the key selects it in an instance.
JSON Schema oneOf does not define a tag representation
JSON Schema commonly represents unions with oneOf. oneOf requires exactly
one subschema to validate, but it does not prescribe a tag representation. A
schema may add a const discriminator property to each branch, use an enclosing
single-property object, or rely entirely on mutually exclusive shapes.
OpenAPI’s discriminator
belongs to the OpenAPI Schema Object and carries OpenAPI mapping behavior; it
is not a general JSON Schema keyword that changes oneOf evaluation. A
conversion must preserve the actual wrapper shape shown here. Adding a
discriminator annotation alone does not do that.
JSON Structure’s choice permits less variation. A tagged choice has one
specified JSON representation, so processors already know how to dispatch it.
Avro uses a similar wrapper
Avro unions are arrays of schemas, and a value must match one branch. In Avro’s JSON encoding, a non-null union value is normally wrapped in an object whose key identifies the selected branch and whose value contains the datum. That is close to JSON Structure’s tagged representation.
The details are not interchangeable. Avro derives the JSON wrapper key from the
branch’s Avro name or primitive type name, and Avro restricts union branch
composition, including duplicate branch types. JSON Structure declares selector
names explicitly as keys in choices, and each key maps to a schema. A converter
must choose and preserve those names rather than assume the two naming systems
are identical.
Avro also has special JSON handling for null in a union: the null value is
encoded directly rather than with a wrapper. JSON Structure’s tagged choice rule
is the one-property object described by core; nullability is a separate type
concern, not an Avro-style exception to the tagged wrapper.
When the selector belongs inside
Core also defines inline choices. Those extend a common abstract base type and
inject a selector property into the selected object. Use that form when the wire
contract calls for {"method":"card", ...} and the variants share a modeled
base.
The payment schema uses the tagged form because its alternatives need no common
base, and each choice may refer to any primitive or compound schema. A consumer
selects card or bankTransfer from the wrapper key before reading or
validating the variant value.