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:
// 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
{
"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:
// 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
{
"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):
{
"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
{
"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):
{
"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
{
"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:
// 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
{
"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):
// 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
{
"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
- Validations —
maxProperties/minPropertiesand the regex-onlypatternProperties:keyword. - Model definitions — the
$refmechanics the typed value schemas rely on.