Document the elements of array parameters and slice fields - #117
Merged
Conversation
An array parameter and a slice field could describe the container but
not what it holds, so a repeated query filter — ?severity=high&
severity=low — documented as an opaque items: {type: string}. Both ways
of asking for more panicked.
On a slice or array field the scalar constraint tags now describe the
ELEMENTS, because an array has no enum, format, pattern, length, or
bound of its own and the flat tag vocabulary cannot tell the two levels
apart:
Severity []string `query:"severity" enum:"info,low,high"`
emits items.enum. That runs through the shared reflection, so bodies,
responses, and webhook payloads get it too. Only minItems, maxItems, and
uniqueItems still describe the array.
The option path says the level out loud instead, with a new ItemOpt
family nested inside ParamItems, which owns the element schema:
stdocs.QueryParam("severity", "array", "Repeated severity filter",
stdocs.ParamItems("string", stdocs.ItemEnum("info", "low", "high")))
Both paths produce the same document. The emitters and tsgen needed no
changes — they already recurse into items — so an element enum renders
as ("info" | "low" | "high")[] in the generated TypeScript, ogen
synthesizes a typed enum for it, and the docs UIs list its values.
default: and example: stay rejected on a slice, and there is no
ItemDefault or ItemExample: a lone value cannot say whether it is the
whole array or one element. Elements that cannot carry a constraint are
named in the panic rather than described as though they were the field.
Two adjacent fixes the change made necessary: Lint could not see an
exclusive bound on a slice's elements, which would have let the
generator-hostile numeric form ship unreported; and a params field whose
elements have no JSON form was accepted with an empty element schema.
Closes #116
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #116.
An array parameter could document its container but not its contents:
?severity=high&severity=lowemitted an opaqueitems: {type: string}, and both ways of asking for more panicked. The array story was half-built — and it was the one place where "Go types become JSON Schemas with validation rules from struct tags" wasn't true.What it does
Tags describe the elements. On a slice or array field the scalar constraints now retarget to
items, because an array has no enum/format/pattern/length/bound of its own and the flat tag vocabulary can't tell the levels apart:This runs through the shared reflection, so bodies, responses and webhook payloads get it too — not just query params. Only
minItems/maxItems/uniqueItemsstill describe the array.Options say the level out loud — a new
ItemOptfamily (ItemEnum,ItemFormat,ItemPattern,ItemMinLength,ItemMaxLength,ItemMinimum,ItemMaximum,ItemExclusiveMinimum,ItemExclusiveMaximum) nested insideParamItems, which owns the element schema:Nesting is load-bearing:
ParamItemsreplacesItemswholesale, so a sibling item option would be silently discarded by a laterParamItems. A new test pins that both paths emit the same document.default:/example:stay rejected on a slice, and there is noItemDefault/ItemExample— a lone value can't say whether it's the whole array or one element. Both panics now explain themselves, andParamEnumon an array points atItemEnum.It came out cheaper than it looks
The emitters and tsgen needed no changes — they already recurse into
items. So this falls out for free:severity?: ("info" | "low" | "high")[](a real union array — verified a bogus member failstsc --strictwith TS2322)GetTasksSeverityItemenumseverity — array string[] · unique! · enumwith the values listedTwo adjacent fixes this made necessary
Lintwas blind to element bounds.lintComponentswalkedPropertiesand neverItems, so[]float64+exclusiveMinimum:"0"would emit the exact generator-hostile numeric 3.1/3.2 formlint.goitself warns about — unreported. It now sees them.ParamFormathad no guard, writingformatonto the array. After the tag change that would have made the two param paths document the same intent differently, breaking the promise atparams.go:22that the modifiers mirror the tags. It now rejects arrays and points atItemFormat.WithParamsfield whose elements have no JSON form ([]func()) was accepted with an empty element schema; and those same elements would have nil-dereferenced under the retarget. Both guarded, and[]func()/[][]stringare now in the fuzz corpus.Verification
gofmt/vet/build/go test -race ./.../golangci-lintclean; nested YAML module passes; fuzzer green over 3M+ execs with the new field kinds.[]*stringitem enum — theanyOf+hoisted-null-enum shape neither corpus had produced.anyOfnullable item enum before the fixture landed — exit 0 on both.tags: ["one"]still type-checks), plus a negative control proving the union is enforced.uismokerenders all nine UIs under the enforced CSP; Scalar spot-checked on a repeated filter.Note for review
[N]bytereflects to{array, items:{integer}}, soformat:"uuid"on a[16]bytenow applies to the integer elements instead of panicking. Accepted deliberately (the schema model can't distinguish it from[]uint16, and format/type coherence is validated nowhere); documented indoc.go+ CHANGELOG, pointing atopenapi:"type=string,format=uuid".