📖 5 min read (~ 1000 words).

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:

Default — inlined
{
  "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

DefaultAllOfForEmbeds — composed
{
  "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 $ref member. Base is a swagger:model, so it has its own definition and composes as {$ref: "#/definitions/Base"} — no copy of id / name.
  • A non-model embed becomes an inline member. Mixin carries no swagger:model, so it has no definition to point at; its note property rides an inline allOf member instead.
  • The embedding struct’s own fields move to a sibling member. color is no longer a top-level property — it lands in its own allOf arm 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 *Base composes to the same $ref member 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 named base`, on or off — Go doesn’t promote a named embed (go-swagger#2038).
  • An explicit swagger:allOf embed already composes, so the flag is a no-op for it; it only makes allOf the default for untagged embeds.
  • Interface embeds compose via allOf regardless 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:

type Decorated struct {
	// swagger:omit ID
	Base

	// ID is assigned by the server.
	//
	// read only: true
	ID int64
}

What’s next