Polymorphic models
Swagger 2.0 expresses polymorphism with three ingredients:
- a base type that declares a
discriminator— the property whose value says which concrete subtype a payload is; - subtypes that include the base via
allOfand add their own fields; - 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.
// 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
{
"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.
// 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
{
"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:
// 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: petResponseFull source: docs/examples/concepts/polymorphism/polymorphism.go
{
"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
PruneUnusedModelsa reachable discriminated base keeps its subtypes, even though no$refreaches them; and an unreachable base is dropped together with its subtypes. You never get a base whose subtypes have vanished. - Only
swagger:allOfcounts. A plain embed inlines the base’s properties instead of composing them, so it is not a subtype relation. TheDefaultAllOfForEmbedsoption 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:
// 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
{
"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:
| level | shape | discriminator |
|---|---|---|
root (Shape) | a plain object | at 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
- Model definitions — the
swagger:allOfcomposition this builds on, and the rest of the model surface. - Routes & operations — return a base type and let the discriminator carry the subtype.
discriminatorkeyword andswagger:allOfreference.