Composing embeds with allOf
When a struct embeds another struct, Go promotes the embedded fields, and by
default codescan mirrors that: the embedded type’s properties are inlined flat
into the embedding schema. That is faithful to the Go value, but it loses the
“this composes Base” relationship — every embedding model emits its own flat
copy of the embedded fields, and a client generator can’t recover the shared
base type.
DefaultAllOfForEmbeds changes that. With the option on, a plain embed (one
with no explicit name and no swagger:allOf tag) is rendered as an allOf
member — exactly as if it carried
swagger:allOf — so the
composition relationship survives in the spec. It is opt-in and defaults to off;
with it off, output is byte-identical to before.
What composes
This model embeds a swagger:model type (Base), a non-model type (Mixin),
and adds an own field:
// Base is a reusable base model.
//
// swagger:model Base
type Base struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
// Mixin is a non-model embedded type (no swagger:model), reachable only through
// embedding. Under the flag it composes as an inline allOf member, since it has
// no definition of its own to $ref.
type Mixin struct {
Note string `json:"note"`
}Full source: docs/examples/shaping/embedallof/embedallof.go
// PlainEmbed embeds a model and a non-model plainly, plus an own field.
//
// swagger:model PlainEmbed
type PlainEmbed struct {
Base
Mixin
Color string `json:"color"`
}Full source: docs/examples/shaping/embedallof/embedallof.go
Scanned with the flag off the embedded properties inline flat; on, the embed
becomes an allOf composition:
{
"type": "object",
"title": "PlainEmbed embeds a model and a non-model plainly, plus an own field.",
"properties": {
"color": {
"type": "string",
"x-go-name": "Color"
},
"id": {
"type": "integer",
"format": "int64",
"x-go-name": "ID"
},
"name": {
"type": "string",
"x-go-name": "Name"
},
"note": {
"type": "string",
"x-go-name": "Note"
}
},
"x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/embedallof"
}
Full source: docs/examples/shaping/embedallof/testdata/plainembed_off.json
{
"title": "PlainEmbed embeds a model and a non-model plainly, plus an own field.",
"allOf": [
{
"$ref": "#/definitions/Base"
},
{
"type": "object",
"properties": {
"note": {
"type": "string",
"x-go-name": "Note"
}
}
},
{
"type": "object",
"properties": {
"color": {
"type": "string",
"x-go-name": "Color"
}
}
}
],
"x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/embedallof"
}
Full source: docs/examples/shaping/embedallof/testdata/plainembed_on.json
Reading the composed pane, each embed takes the path its kind dictates:
- A model embed becomes a
$refmember.Baseis aswagger:model, so it has its own definition and composes as{$ref: "#/definitions/Base"}— no copy ofid/name. - A non-model embed becomes an inline member.
Mixincarries noswagger:model, so it has no definition to point at; itsnoteproperty rides an inlineallOfmember instead. - The embedding struct’s own fields move to a sibling member.
coloris no longer a top-level property — it lands in its ownallOfarm alongside the composed embeds.
What’s left alone
The flag only changes the untagged, unnamed embed — every other embed shape is unaffected:
// PointerEmbed embeds a model through a pointer; the pointer is peeled and
// takes the same $ref path as a value embed.
//
// swagger:model PointerEmbed
type PointerEmbed struct {
*Base
Tag string `json:"tag"`
}
// NamedEmbed embeds Base under an explicit json name, so Go does not promote
// it: it stays a single nested property, identical on or off (go-swagger#2038).
//
// swagger:model NamedEmbed
type NamedEmbed struct {
Base `json:"base"`
Extra string `json:"extra"`
}
// TaggedEmbed already composes Base via an explicit swagger:allOf tag, so the
// flag does not change its shape.
//
// swagger:model TaggedEmbed
type TaggedEmbed struct {
// swagger:allOf
Base
Field string `json:"field"`
}Full source: docs/examples/shaping/embedallof/embedallof.go
- Pointer embeds are peeled first, so
*Basecomposes to the same$refmember as a value embed. - A json-named embed is not a promotion. Giving the embed a json tag
(
Base \json:“base”`) makes it a single nested property namedbase`, on or off — Go doesn’t promote a named embed (go-swagger#2038). - An explicit
swagger:allOfembed already composes, so the flag is a no-op for it; it only makesallOfthe default for untagged embeds. - Interface embeds compose via
allOfregardless of this flag.
Note
DefaultAllOfForEmbeds is the global default-on switch for the same shape
swagger:allOf produces per-embed. Reach for the annotation when only some
embeds should compose; reach for the option when composition is your house style
for every plain embed.
When an override cannot be composed
Composition has one limit worth knowing. Inlining an embed resolves an
override — a field the enclosing struct re-declares wins, exactly as Go’s depth
rule decides it. allOf instead accumulates: members conjoin, and a
conjunction can only narrow, never replace. So a re-declaration that replaces
is not expressible as composition:
- re-declaring a promoted field to decorate it (add
readOnly, a description, a validation) leaves the property in both members — valid, but a generator walking the members sees it twice; - re-declaring it with a different type yields
{type: integer}and{type: string}for one property — a schema nothing can satisfy.
codescan does not guess which declaration you meant: many Go types can be
written whose composition has no faithful schema, and inventing one would be
deciding your intent. Resolve it yourself with
swagger:omit on the
embed, which drops the promoted twin so only your re-declaration survives:
What’s next
swagger:omit— drop what an embed promotes but the API should not carry.- Polymorphic models — the
swagger:allOfannotation and discriminator hints this option generalises. - Descriptions beside a
$ref— how a description and validations render on anallOfmember. - Resolving
$refname conflicts — the definition names the composed$refs point at.