📖 8 min read (~ 1600 words).

Maps & free-form objects

Not every object has a fixed set of named fields. A Go map models an object with dynamic keys; a struct can be marked open (extra keys allowed), closed (extra keys forbidden), or given a typed value schema for its extras. codescan expresses all of this with additionalProperties and patternProperties. The panes below are rendered from the test-covered docs/examples/concepts/maps package.

Maps become objects

A map field renders as {type: object} whose values all share one schema, carried as additionalProperties — the value schema is derived from the Go map’s element type:

Annotated Go
// Inventory shows a plain Go map. A map renders as an object whose values all
// share one schema, carried as additionalProperties.
//
// swagger:model
type Inventory struct {
	// Counts maps each SKU to its on-hand quantity.
	Counts map[string]int `json:"counts"`
}

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

#/definitions/Inventory
{
  "description": "Inventory shows a plain Go map. A map renders as an object whose values all\nshare one schema, carried as additionalProperties.",
  "type": "object",
  "properties": {
    "counts": {
      "description": "Counts maps each SKU to its on-hand quantity.",
      "type": "object",
      "additionalProperties": {
        "type": "integer",
        "format": "int64"
      },
      "x-go-name": "Counts"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/maps"
}

Full source: docs/examples/concepts/maps/testdata/naturalmap.json

Which map keys work

A map key has to become a JSON object key — a string. codescan accepts every key type encoding/json can stringify: string kinds, every integer / unsigned kind (so map[int]V, map[uint8]V, …), and any type implementing encoding.TextMarshaler. An integer-keyed map is therefore still a valid object:

Annotated Go
// Lookups shows which Go map keys survive. A key is usable when encoding/json
// can stringify it: string kinds, every integer/unsigned kind, and types
// implementing encoding.TextMarshaler. Other keys (float, bool, struct without
// TextMarshaler) are dropped with a diagnostic.
//
// swagger:model
type Lookups struct {
	// ByName is keyed by a plain string.
	ByName map[string]int `json:"byName"`

	// ByCode is keyed by an integer — JSON stringifies it, so the map is still
	// an object with additionalProperties.
	ByCode map[int]string `json:"byCode"`
}

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

#/definitions/Lookups
{
  "description": "Lookups shows which Go map keys survive. A key is usable when encoding/json\ncan stringify it: string kinds, every integer/unsigned kind, and types\nimplementing encoding.TextMarshaler. Other keys (float, bool, struct without\nTextMarshaler) are dropped with a diagnostic.",
  "type": "object",
  "properties": {
    "byCode": {
      "description": "ByCode is keyed by an integer — JSON stringifies it, so the map is still\nan object with additionalProperties.",
      "type": "object",
      "additionalProperties": {
        "type": "string"
      },
      "x-go-name": "ByCode"
    },
    "byName": {
      "description": "ByName is keyed by a plain string.",
      "type": "object",
      "additionalProperties": {
        "type": "integer",
        "format": "int64"
      },
      "x-go-name": "ByName"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/maps"
}

Full source: docs/examples/concepts/maps/testdata/keytypes.json

Info

A key JSON cannot stringify — a float, bool, a struct without TextMarshaler, or an interface — is not silently dropped: the map’s additionalProperties is omitted and a CodeUnsupportedType diagnostic (“additionalProperties dropped”) is raised. A json:"-" map field is muted before this check, so it never warns.

Open & closed objects

A struct normally renders with just its named properties and says nothing about extra keys. The decl-level swagger:additionalProperties <spec> marker decides the policy, where <spec> is true, false, or a value type:

// ClosedObject forbids any key beyond its named properties: the marker false
// closes the object.
//
// swagger:model
// swagger:additionalProperties false
type ClosedObject struct {
	A string `json:"a"`
	B int    `json:"b"`
}

// OpenObject keeps its named property and also allows arbitrary extra keys.
//
// swagger:model
// swagger:additionalProperties true
type OpenObject struct {
	A string `json:"a"`
}

// TypedObject complements its named property with typed (integer) extra values.
//
// swagger:model
// swagger:additionalProperties integer
type TypedObject struct {
	A string `json:"a"`
}

// RefObject references a model as the schema of its extra values.
//
// swagger:model
// swagger:additionalProperties Thing
type RefObject struct {
	A string `json:"a"`
}

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

false closes the object (only the named properties are valid); true opens it (any extra key is allowed):

false — closed object
{
  "description": "ClosedObject forbids any key beyond its named properties: the marker false\ncloses the object.",
  "type": "object",
  "properties": {
    "a": {
      "type": "string",
      "x-go-name": "A"
    },
    "b": {
      "type": "integer",
      "format": "int64",
      "x-go-name": "B"
    }
  },
  "additionalProperties": false,
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/maps"
}

Full source: docs/examples/concepts/maps/testdata/closed.json

true — open object
{
  "type": "object",
  "title": "OpenObject keeps its named property and also allows arbitrary extra keys.",
  "properties": {
    "a": {
      "type": "string",
      "x-go-name": "A"
    }
  },
  "additionalProperties": true,
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/maps"
}

Full source: docs/examples/concepts/maps/testdata/open.json

A type spec instead of a bool gives the extra values a schema — a primitive (or []T), or a model name that becomes a $ref (the same value-type grammar as swagger:type, except a type name resolves to a $ref):

integer — typed values
{
  "type": "object",
  "title": "TypedObject complements its named property with typed (integer) extra values.",
  "properties": {
    "a": {
      "type": "string",
      "x-go-name": "A"
    }
  },
  "additionalProperties": {
    "type": "integer"
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/maps"
}

Full source: docs/examples/concepts/maps/testdata/typed.json

Thing — model values
{
  "type": "object",
  "title": "RefObject references a model as the schema of its extra values.",
  "properties": {
    "a": {
      "type": "string",
      "x-go-name": "A"
    }
  },
  "additionalProperties": {
    "$ref": "#/definitions/Thing"
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/maps"
}

Full source: docs/examples/concepts/maps/testdata/ref.json

On a map type the marker overrides the element-derived value schema; on a struct it complements the named properties (as above). It composes with the object validations maxProperties / minProperties / patternProperties.

Info

The model name is a bare leaf: codescan resolves it in the annotating type’s own package first, then uniquely across the scanned model set, so a value type declared in another package resolves to a $ref by name. A leaf that matches a model in several packages is ambiguous — it is dropped with a validate.ambiguous-type-name diagnostic. See Resolving $ref name conflicts.

Per-field control

The same <spec> is available as a field keyword, additionalProperties: <spec>, decorating one struct field. On a map field it overrides the value schema; on a $ref’d field the value rides an allOf sibling so the reference is preserved:

Annotated Go
// Holder decorates individual fields with the additionalProperties: keyword.
//
// swagger:model
type Holder struct {
	// OverriddenMap keeps its map shape but overrides the value schema from the
	// Go element type (string) to integer.
	//
	// additionalProperties: integer
	OverriddenMap map[string]string `json:"overriddenMap"`

	// RefMap points the map values at a model.
	//
	// additionalProperties: Thing
	RefMap map[string]string `json:"refMap"`

	// ClosedRef references a model and forbids extra keys. Because the field is
	// a $ref, the value rides an allOf sibling so the reference is preserved.
	//
	// additionalProperties: false
	ClosedRef Thing `json:"closedRef"`
}

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

#/definitions/Holder
{
  "type": "object",
  "title": "Holder decorates individual fields with the additionalProperties: keyword.",
  "properties": {
    "closedRef": {
      "description": "ClosedRef references a model and forbids extra keys. Because the field is\na $ref, the value rides an allOf sibling so the reference is preserved.",
      "allOf": [
        {
          "$ref": "#/definitions/Thing"
        },
        {
          "additionalProperties": false
        }
      ],
      "x-go-name": "ClosedRef"
    },
    "overriddenMap": {
      "description": "OverriddenMap keeps its map shape but overrides the value schema from the\nGo element type (string) to integer.",
      "type": "object",
      "additionalProperties": {
        "type": "integer"
      },
      "x-go-name": "OverriddenMap"
    },
    "refMap": {
      "description": "RefMap points the map values at a model.",
      "type": "object",
      "additionalProperties": {
        "$ref": "#/definitions/Thing"
      },
      "x-go-name": "RefMap"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/maps"
}

Full source: docs/examples/concepts/maps/testdata/fieldkeyword.json

Pattern properties

patternProperties constrains extra keys by a name regex rather than allowing all of them. The regex-only patternProperties: keyword maps a pattern to an empty (any-value) schema. The decl-level swagger:patternProperties "<re>": <spec>, … marker is the typed counterpart — each quoted regex pairs with a value spec (a primitive, or a model name that becomes a $ref):

Annotated Go
// TypedPatterns maps property-name regexes to typed value schemas. Each quoted
// regex pairs with a value spec — a primitive or a model name (which becomes a
// $ref). The pattern-properties keyword is JSON-Schema, beyond the Swagger 2.0
// subset; codescan emits it ungated.
//
// swagger:model
// swagger:patternProperties "^x-": string, "^\d+$": integer, "^item-": Thing
type TypedPatterns struct {
	Known string `json:"known"`
}

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

#/definitions/TypedPatterns
{
  "description": "TypedPatterns maps property-name regexes to typed value schemas. Each quoted\nregex pairs with a value spec — a primitive or a model name (which becomes a\n$ref). The pattern-properties keyword is JSON-Schema, beyond the Swagger 2.0\nsubset; codescan emits it ungated.",
  "type": "object",
  "properties": {
    "known": {
      "type": "string",
      "x-go-name": "Known"
    }
  },
  "patternProperties": {
    "^\\d+$": {
      "type": "integer"
    },
    "^item-": {
      "$ref": "#/definitions/Thing"
    },
    "^x-": {
      "type": "string"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/maps"
}

Full source: docs/examples/concepts/maps/testdata/patterntyped.json

Note

Beyond Swagger 2.0. patternProperties is a JSON-Schema (draft-4) keyword, not part of the Swagger 2.0 Schema Object subset. codescan emits it ungated, consistent with go-openapi’s JSON-Schema-first stance — your downstream tooling must understand it. Each regex is RE2-hygiene-checked: one that does not compile raises a CodeInvalidAnnotation warning but is preserved on the schema.

Info

additionalProperties and patternProperties are object-schema keywords — they only ride on an object. They are the lowest-priority annotations: if a prior rule already fixed a non-object type (a swagger:type scalar, a swagger:strfmt, a special type), the marker is dropped with a CodeShapeMismatch diagnostic. Object schemas also have no OAS-2 SimpleSchema form, so neither keyword applies on a non-body parameter or a response header (it is dropped there with a diagnostic).

What’s next

  • ValidationsmaxProperties / minProperties and the regex-only patternProperties: keyword.
  • Model definitions — the $ref mechanics the typed value schemas rely on.