📖 8 min read (~ 1600 words).

Polymorphic models

Swagger 2.0 expresses polymorphism with three ingredients:

  1. a base type that declares a discriminator — the property whose value says which concrete subtype a payload is;
  2. subtypes that include the base via allOf and add their own fields;
  3. a discriminator value per subtype (here, the subtype’s definition name).

The panes below are backed by the test-covered docs/examples/concepts/polymorphism package.

The base type

Mark one property discriminator: true. codescan writes that property’s name onto the schema’s discriminator. A discriminator property must also be required — a consumer cannot pick a subtype from a value that may be absent.

Annotated Go
// Pet is the polymorphic base type. The field marked `discriminator: true` names
// the property whose value tells a consumer which concrete subtype a payload is:
// codescan writes that property's name onto the schema's `discriminator`, and a
// discriminator property must be `required`.
//
// swagger:model
type Pet struct {
	// PetType selects the concrete subtype — its value is the subtype's
	// definition name (e.g. "Cat" or "Dog").
	//
	// discriminator: true
	// required: true
	PetType string `json:"petType"`

	// Name is common to every pet.
	//
	// required: true
	Name string `json:"name"`
}

Full source: docs/examples/concepts/polymorphism/polymorphism.go

#/definitions/Pet
{
  "description": "Pet is the polymorphic base type. The field marked `discriminator: true` names\nthe property whose value tells a consumer which concrete subtype a payload is:\ncodescan writes that property's name onto the schema's `discriminator`, and a\ndiscriminator property must be `required`.",
  "type": "object",
  "required": [
    "petType",
    "name"
  ],
  "properties": {
    "name": {
      "description": "Name is common to every pet.",
      "type": "string",
      "x-go-name": "Name"
    },
    "petType": {
      "description": "PetType selects the concrete subtype — its value is the subtype's\ndefinition name (e.g. \"Cat\" or \"Dog\").",
      "type": "string",
      "x-go-name": "PetType"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/polymorphism",
  "discriminator": "petType"
}

Full source: docs/examples/concepts/polymorphism/testdata/base.json

The subtypes

Each subtype embeds the base as an anonymous field annotated swagger:allOf. The result is allOf: [ {$ref to the base}, {the subtype's own fields} ] — the same composition covered in Model definitions, now given meaning by the base’s discriminator.

Annotated Go
// Cat is a Pet subtype. `swagger:allOf` composes it as
// `allOf: [ $ref Pet, {its own fields} ]` — the composition Swagger 2.0
// polymorphism builds on.
//
// swagger:model
type Cat struct {
	// swagger:allOf
	Pet

	// HuntingSkill is how the cat hunts.
	HuntingSkill string `json:"huntingSkill"`
}

// Dog is a second Pet subtype.
//
// swagger:model
type Dog struct {
	// swagger:allOf
	Pet

	// PackSize is the size of the dog's pack.
	PackSize int32 `json:"packSize"`
}

Full source: docs/examples/concepts/polymorphism/polymorphism.go

#/definitions/Cat
{
  "description": "Cat is a Pet subtype. `swagger:allOf` composes it as\n`allOf: [ $ref Pet, {its own fields} ]` — the composition Swagger 2.0\npolymorphism builds on.",
  "allOf": [
    {
      "$ref": "#/definitions/Pet"
    },
    {
      "type": "object",
      "properties": {
        "huntingSkill": {
          "description": "HuntingSkill is how the cat hunts.",
          "type": "string",
          "x-go-name": "HuntingSkill"
        }
      }
    }
  ],
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/polymorphism"
}

Full source: docs/examples/concepts/polymorphism/testdata/subtype.json

Dog follows the identical shape. A payload is then recognised as a Cat or a Dog by its petType value.

How subtypes are discovered

A family has an awkward property: the references all point upwards. A subtype $refs its base, and nothing ever $refs a subtype. So an API that returns the base — the whole point of polymorphism — names only the base, and the ordinary reachability rule would stop right there, leaving a discriminator with nothing to discriminate between.

codescan therefore looks the relation up backwards. When a definition that declares a discriminator enters the spec, every swagger:model that composes it under swagger:allOf is pulled in with it — wherever those subtypes are declared, including other packages. No ScanModels needed. The route below references only Pet:

Annotated Go
// PetResponse returns the polymorphic BASE — nothing in the API surface names
// Cat or Dog.
//
// swagger:response petResponse
type PetResponse struct {
	// in: body
	Body Pet `json:"body"`
}

// swagger:route GET /pets pets listPets
//
// Lists pets.
//
// responses:
//
//	200: petResponse

Full source: docs/examples/concepts/polymorphism/polymorphism.go

Whole spec, scanned WITHOUT ScanModels
{
  "swagger": "2.0",
  "paths": {
    "/pets": {
      "get": {
        "tags": [
          "pets"
        ],
        "summary": "Lists pets.",
        "operationId": "listPets",
        "responses": {
          "200": {
            "$ref": "#/responses/petResponse"
          }
        }
      }
    }
  },
  "definitions": {
    "Cat": {
      "description": "Cat is a Pet subtype. `swagger:allOf` composes it as\n`allOf: [ $ref Pet, {its own fields} ]` — the composition Swagger 2.0\npolymorphism builds on.",
      "allOf": [
        {
          "$ref": "#/definitions/Pet"
        },
        {
          "type": "object",
          "properties": {
            "huntingSkill": {
              "description": "HuntingSkill is how the cat hunts.",
              "type": "string",
              "x-go-name": "HuntingSkill"
            }
          }
        }
      ],
      "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/polymorphism"
    },
    "Dog": {
      "title": "Dog is a second Pet subtype.",
      "allOf": [
        {
          "$ref": "#/definitions/Pet"
        },
        {
          "type": "object",
          "properties": {
            "packSize": {
              "description": "PackSize is the size of the dog's pack.",
              "type": "integer",
              "format": "int32",
              "x-go-name": "PackSize"
            }
          }
        }
      ],
      "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/polymorphism"
    },
    "Pet": {
      "description": "Pet is the polymorphic base type. The field marked `discriminator: true` names\nthe property whose value tells a consumer which concrete subtype a payload is:\ncodescan writes that property's name onto the schema's `discriminator`, and a\ndiscriminator property must be `required`.",
      "type": "object",
      "required": [
        "petType",
        "name"
      ],
      "properties": {
        "name": {
          "description": "Name is common to every pet.",
          "type": "string",
          "x-go-name": "Name"
        },
        "petType": {
          "description": "PetType selects the concrete subtype — its value is the subtype's\ndefinition name (e.g. \"Cat\" or \"Dog\").",
          "type": "string",
          "x-go-name": "PetType"
        }
      },
      "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/polymorphism",
      "discriminator": "petType"
    }
  },
  "responses": {
    "petResponse": {
      "description": "PetResponse returns the polymorphic BASE — nothing in the API surface names\nCat or Dog.",
      "schema": {
        "$ref": "#/definitions/Pet"
      }
    }
  }
}

Full source: docs/examples/concepts/polymorphism/testdata/spec-reachable-only.json

Cat and Dog are in there, and each pull is announced on the OnDiagnostic sink, so a definition you did not ask for by name is never a mystery:

scan.discovered-subtype: definition "Cat" discovered as a subtype of discriminated base "Pet"
scan.discovered-subtype: definition "Dog" discovered as a subtype of discriminated base "Pet"

Full source: docs/examples/concepts/polymorphism/testdata/hints.txt

Three consequences worth knowing:

  • Reachability, not existence. The trigger is the base entering the spec, not merely existing in the scanned source. A discriminated base that nothing references still emits nothing — bases are not roots, or every hierarchy in a shared library would land in every spec.
  • The family travels as a unit. With PruneUnusedModels a reachable discriminated base keeps its subtypes, even though no $ref reaches them; and an unreachable base is dropped together with its subtypes. You never get a base whose subtypes have vanished.
  • Only swagger:allOf counts. A plain embed inlines the base’s properties instead of composing them, so it is not a subtype relation. The DefaultAllOfForEmbeds option changes how embeds render, deliberately not which definitions exist.

Multi-level hierarchies

A subtype can be a base in its own right. Write the intermediate level as an interface — only an interface can be embedded by the concrete structs beneath it — composing its parent with swagger:allOf and declaring a discriminator of its own:

Annotated Go
// Shape is the root of the hierarchy: a base written as an interface, whose
// `discriminator: true` member names the property a consumer switches on.
//
// swagger:model
type Shape interface {
	// ShapeType selects the concrete subtype.
	//
	// discriminator: true
	// required: true
	// swagger:name shapeType
	ShapeType() string

	// swagger:name area
	Area() float64
}

// Polygon is a subtype of Shape AND a base of its own: it composes Shape as an
// allOf member and declares a second discriminator. An intermediate level is
// written as an interface, because only an interface can be embedded by the
// concrete structs below it.
//
// swagger:model
type Polygon interface {
	// swagger:allOf
	Shape

	// PolygonType selects the concrete polygon.
	//
	// discriminator: true
	// required: true
	// swagger:name polygonType
	PolygonType() string
}

// Square is a leaf, two levels down. It composes the INTERMEDIATE type, so it
// inherits Shape transitively.
//
// swagger:model
type Square struct {
	// swagger:allOf
	Polygon

	// Side is the length of a side.
	Side float64 `json:"side"`
}

Full source: docs/examples/concepts/polymorphism-nested/nested.go

#/definitions/Polygon — subtype AND base
{
  "description": "Polygon is a subtype of Shape AND a base of its own: it composes Shape as an\nallOf member and declares a second discriminator. An intermediate level is\nwritten as an interface, because only an interface can be embedded by the\nconcrete structs below it.",
  "allOf": [
    {
      "$ref": "#/definitions/Shape"
    },
    {
      "type": "object",
      "required": [
        "polygonType"
      ],
      "properties": {
        "polygonType": {
          "description": "PolygonType selects the concrete polygon.",
          "type": "string",
          "x-go-name": "PolygonType"
        }
      },
      "discriminator": "polygonType"
    }
  ],
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/polymorphism-nested"
}

Full source: docs/examples/concepts/polymorphism-nested/testdata/intermediate.json

Discovery cascades: the route references Shape, Shape pulls Polygon, and Polygon — itself only just discovered — pulls Square. Note where each level’s discriminator lands, because the two differ:

levelshapediscriminator
root (Shape)a plain objectat the top level
intermediate (Polygon)allOf: [ $ref Shape, {own} ]inside its own allOf member
leaf (Square)allOf: [ $ref Polygon, {own} ]none of its own

A leaf never inherits its base’s discriminator as its own: it points at a discriminated base, which is what makes it a subtype, not a base.

Info

The discriminator value for each subtype is its definition name (Cat, Dog) — so petType must carry exactly "Cat" or "Dog". codescan does not implement a custom-value annotation (swagger:discriminatorValue), so the subtype name is the value. Keep the discriminator a plain string and required on the base; it is inherited by every subtype through the $ref.

What’s next