Specification scope: Core only.

Consider an address object with city, state, and zip. It may also carry a street, or it may carry a post office box number. Those optional properties do not state which variant the producer selected. An object can contain both properties, and a future variant may overlap with either existing shape.

An inline choice puts the decision in the data. Its variants share an abstract base, and a string property in the object names the selected variant. The address remains one flat JSON object; no wrapper is added.

The wire shape

A choice is JSON Structure’s discriminated union type. Its choices map associates selector values with types. In the tagged form, a property wraps the selected value. In the inline form used here, the selected object’s members remain in place and the selector sits beside them.

For addresses, that means an instance looks like this:

{
  "addressType": "StreetAddress",
  "street": "123 Main St",
  "city": "Seattle",
  "state": "WA",
  "zip": "98101"
}

There is no { "StreetAddress": { ... } } wrapper. addressType has the value StreetAddress, so a processor uses that entry from choices.

The complete schema

{
  "$schema": "https://json-structure.org/meta/core/v0/#",
  "$id": "https://schemas.example.com/address.json",
  "name": "AddressChoice",
  "type": "choice",
  "$extends": "#/definitions/Address",
  "selector": "addressType",
  "choices": {
    "StreetAddress": {
      "type": { "$ref": "#/definitions/StreetAddress" }
    },
    "PostOfficeBoxAddress": {
      "type": { "$ref": "#/definitions/PostOfficeBoxAddress" }
    }
  },
  "definitions": {
    "Address": {
      "abstract": true,
      "type": "object",
      "properties": {
        "city": { "type": "string" },
        "state": { "type": "string" },
        "zip": { "type": "string" }
      }
    },
    "StreetAddress": {
      "type": "object",
      "$extends": "#/definitions/Address",
      "properties": {
        "street": { "type": "string" }
      }
    },
    "PostOfficeBoxAddress": {
      "type": "object",
      "$extends": "#/definitions/Address",
      "properties": {
        "poBox": { "type": "string" }
      }
    }
  }
}

$extends appears in two roles here. On StreetAddress and PostOfficeBoxAddress, it merges the properties of Address into each concrete definition. StreetAddress therefore has city, state, zip, and street; an extending type may not redefine an inherited property.

On the choice, $extends identifies the common base required for the inline representation. That base must be abstract, and every selected type must extend it. Address can supply reusable properties, but it cannot itself be used as a property type or referenced through $ref.

Selection is declared, not inferred

The selector keyword names the injected string property. Its value must match a key in choices exactly. In this schema, the only selector values are StreetAddress and PostOfficeBoxAddress.

Suppose an object contains city, state, zip, street, and poBox. Property inspection cannot tell you which variant the producer meant. The selector names the branch to validate, so the processor does not rank the two shapes.

The selector may shadow a property from the base, but only when that inherited property is a string. Do not declare it in the base unless the base itself needs that string property; the inline choice injects the selector.

Choice names and selector values are case-sensitive. streetAddress does not select StreetAddress.

No subtype assignment

$extends supports property reuse, but JSON Structure deliberately does not turn the base into a polymorphic assignment target. You cannot declare a property as Address and then place either concrete subtype there. An abstract type cannot be referenced through $ref at all.

Use the choice as the property’s type when a property may hold either address:

{
  "type": "object",
  "properties": {
    "shippingAddress": {
      "type": { "$ref": "#/definitions/AddressChoice" }
    }
  }
}

Here AddressChoice would be a reusable choice under definitions, using the same construction as the root example above.

The base records shared members. The choice records which concrete values may occur and how an instance selects one. Keep those jobs separate: declaring a base does not introduce subtype assignment elsewhere in the model.