Skip to content

Describe map-of-slice values via additionalProperties#108

Merged
seanogdev merged 4 commits intomasterfrom
map-of-slice-additional-properties
Apr 23, 2026
Merged

Describe map-of-slice values via additionalProperties#108
seanogdev merged 4 commits intomasterfrom
map-of-slice-additional-properties

Conversation

@seanogdev
Copy link
Copy Markdown
Contributor

@seanogdev seanogdev commented Apr 23, 2026

What this does

Fixes a gap in *ast.MapType handling. Fields typed as map[K][]T previously dropped the element type — they rendered as type: object with no additionalProperties, which downstream tools (openapi-typescript, etc.) flatten to Record<string, never>.

The value-type resolver (findTypeIdent) only handles Ident/SelectorExpr, so a slice value fell through the error branch. This PR detects *ast.ArrayType values up front and builds an array schema via the existing resolveArray, then attaches it as additionalProperties.

While here, prefixPropertyReferences now recurses into nested items and additionalProperties, so $ref values inside the new array schema get the #/definitions/... prefix.

Before / after

Given map[int64][]SomeStruct:

Before

foo:
  type: object

After

foo:
  type: object
  additionalProperties:
    type: array
    items:
      $ref: '#/definitions/pkg.SomeStruct'

Tests

Extended the existing struct-map fixture with three new fields covering primitive, local-struct, and cross-package-struct slice element types. All existing tests still pass.

Why

map[K][]T is a natural Go shape for "group these items by X" response payloads. Without this fix, any such field ends up as an opaque object in the spec and consumers lose all type information about the slice elements. Fixing it here avoids per-field workarounds in downstream type generators.

@coveralls
Copy link
Copy Markdown

coveralls commented Apr 23, 2026

Coverage Report for CI Build 24847921383

Coverage decreased (-0.1%) to 52.863%

Details

  • Coverage decreased (-0.1%) from the base build.
  • Patch coverage: 35 uncovered changes across 2 files (21 of 56 lines covered, 37.5%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
docparse/jsonschema.go 43 10 23.26%
openapi2/openapi2.go 13 11 84.62%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 2183
Covered Lines: 1154
Line Coverage: 52.86%
Coverage Strength: 44.63 hits per line

💛 - Coveralls

@seanogdev seanogdev requested a review from rafaeljusto April 23, 2026 14:25
@seanogdev seanogdev assigned marcizhu and unassigned marcizhu Apr 23, 2026
@seanogdev seanogdev requested a review from marcizhu April 23, 2026 14:29
@seanogdev seanogdev marked this pull request as ready for review April 23, 2026 14:33
PrimSlices map[string][]string `json:"primSlices"` // Map of primitive slices.
StructSlice map[int64][]aStruct `json:"structSlice"` // Map of local struct slices.
OtherSlice map[string][]otherpkg.OtherStruct `json:"otherSlice"` // Map of cross-package struct slices.
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the same happens for a map of maps but honestly I don't know if it should be addressed.
maps are too opened for web apis and feel lazy

Suggested change
}
OtherMap map[string]map[string]otherpkg.OtherStruct `json:"otherMap"` // Other map comment.
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added nested map support there, although you'd be best positioned to know how it might impact performance .

@seanogdev seanogdev requested a review from cesartw April 23, 2026 16:29
The `*ast.MapType` handler only resolved map values of kind
`*ast.Ident`/`*ast.SelectorExpr`, so fields typed as `map[K][]T` silently
dropped the element type and rendered as `type: object` with no
`additionalProperties`. Consumers (openapi-typescript, etc.) then saw an
opaque `Record<string, never>` where the server actually emits a map of
arrays.

Detect a slice value up front and build an array schema for it via
`resolveArray`, attaching it as `additionalProperties`. While here, make
`prefixPropertyReferences` recurse into nested `additionalProperties`
and `items` so references inside the new array schema get the
`#/definitions/...` prefix they need.

Fixture `struct-map` grows three new cases covering primitive, local, and
cross-package slice element types.
Keeps fieldToSchema's cyclomatic complexity under the gocyclo threshold
(65). Pure refactor — no behavior change, no new tests needed.
A `map[K]map[K2]V` field previously dropped the inner map's value type:
`findTypeIdent` only handles `Ident`/`SelectorExpr`, so a map-typed value
fell through to the open-object fallback. Detect `*ast.MapType` up front
and recurse, producing nested `additionalProperties`.

`prefixSchemaReferences` already walks `AdditionalProperties`, so `$ref`s
inside the nested schema are rewritten correctly without further changes.
`resolveMap` compared the raw Go type name against the JSON-schema
primitive set, so `int`/`bool`/`float64` failed the check and fell
through to an open-object fallback. Run `JSONSchemaType` first so
`int`→`integer`, `bool`→`boolean`, etc. match.
@seanogdev seanogdev force-pushed the map-of-slice-additional-properties branch from bda12c4 to 4c622b1 Compare April 23, 2026 16:58
@seanogdev seanogdev merged commit 7f9292c into master Apr 23, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants