Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d53056d
test(schema): map swagger:strfmt handling across every alias dispatch…
fredbi Aug 1, 2026
1b0e7b2
fix(schema): honour swagger:strfmt on an alias declaration
fredbi Aug 1, 2026
9d92874
fix(schema): decide a sequence's strfmt placement by its element type
fredbi Aug 1, 2026
4de3a6e
fix(schema): retire swagger:default, which never emitted a default
fredbi Aug 2, 2026
b72a43d
fix(schema): type default, example and enum values on a declaration
fredbi Aug 2, 2026
23a9d1e
fix(schema): report values that cannot be read as their schema type
fredbi Aug 2, 2026
53747ca
fix(schema): emit the properties encoding/json actually marshals
fredbi Aug 2, 2026
5ad1df1
fix(schema): honour swagger:type on an alias, and accept file where O…
fredbi Aug 2, 2026
9de2313
fix(grammar): stop rejecting in: and name: beside a classifier annota…
fredbi Aug 2, 2026
ed9897c
fix(schema): report a swagger:enum an alias can never host
fredbi Aug 2, 2026
4779aa0
fix(schema): honour an alias declaration in body positions, and detec…
fredbi Aug 2, 2026
3407a14
refact(builders): share one alias-field resolution across parameters …
fredbi Aug 2, 2026
0fd15d4
test(builders): cover the field-dispatch arms and the response items …
fredbi Aug 2, 2026
3848dac
refact(builders): share the schema sub-build the field dispatch repeats
fredbi Aug 2, 2026
fc29d3c
fix(parameters): drop an error-typed parameter instead of failing the…
fredbi Aug 2, 2026
7b7f0f5
fix(builders): let the field dispatch reach the element-aware format …
fredbi Aug 2, 2026
1a353da
fix(responses): attach the schema a named non-struct response builds
fredbi Aug 2, 2026
7b3f3c1
fix(responses): follow a declaration's written right-hand side, not i…
fredbi Aug 2, 2026
7ff038a
fix(builders): run the canonical stdlib recognizers in the field arms
fredbi Aug 2, 2026
361e2f2
fix(responses): write a body's vendor extensions into its schema
fredbi Aug 2, 2026
6ce3bd4
fix(parsers): accept a one-character tag or operationId in a route
fredbi Aug 2, 2026
e666037
feat(scanner): report a path annotation that does not parse
fredbi Aug 2, 2026
728b8f6
test(builders): compare an allOf member against the other full-schema…
fredbi Aug 2, 2026
e7e4e5c
fix(scanner): skip an unrecognised annotation instead of failing the …
fredbi Aug 2, 2026
1973da5
feat(schema): report a classifier annotation on an embed as ineffective
fredbi Aug 2, 2026
3fa98ee
doc: say that a classifier annotation on an embed is ignored
fredbi Aug 2, 2026
2c350a5
fix(schema): run the classifier cascade on an allOf member
fredbi Aug 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions cmd/genspec-tui/internal/ux/model_diagmarks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,20 +115,27 @@ func TestE2E_DiagnosticMarksTheOffendingKeyword(t *testing.T) {
"line %d: a mark past the end of the line it is on", mark.Line+1)
}

// `// in: formData` is reported at the keyword; after translation it must
// still be the keyword, not the tab that precedes it.
// `// maximum: 3` is a schema keyword under a prose-only classifier body, so it is reported at
// the keyword; after translation the mark must still be the keyword, not the tab that precedes
// it.
//
// The subject used to be the `// in: formData` on the same field, which warned beside
// `swagger:file`. That warning was spurious — `in:` is a field directive the parameters builder
// reads out of band, and the line it fired on is the canonical file-upload idiom — so the subject
// moved to a keyword that is genuinely invalid there. The tab-indent property under test is
// unchanged.
source := strings.Split(m.currentSource, "\n")
var checked int
for _, d := range m.diags {
if d.Pos.Filename != path || !strings.Contains(source[d.Pos.Line-1], "// in:") {
if d.Pos.Filename != path || !strings.Contains(source[d.Pos.Line-1], "// maximum:") {
continue
}
col := bufferColumn(source[d.Pos.Line-1], d.Pos.Column)
assert.True(t, strings.HasPrefix(string([]rune(buffer[d.Pos.Line-1])[col-1:]), "in:"),
assert.True(t, strings.HasPrefix(string([]rune(buffer[d.Pos.Line-1])[col-1:]), "maximum:"),
"line %d landed on %q", d.Pos.Line, buffer[d.Pos.Line-1])
checked++
}
require.Positive(t, checked, "the fixture must still contain a context-invalid `in:`")
require.Positive(t, checked, "the fixture must still contain a context-invalid keyword")
}

// The mark has to survive all the way to the screen, over the lexical class the
Expand Down
2 changes: 1 addition & 1 deletion docs/doc-site/annotation-index/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ tutorial that shows the annotation as runnable Go next to the spec it produces;
| `swagger:additionalProperties` | type doc | object `additionalProperties` (open / closed / typed) | [example]({{% relref "/tutorials/maps-and-free-form-objects#open--closed-objects" %}}) | [reference]({{% relref "/maintainers/annotations/swagger-additionalproperties" %}}) |
| `swagger:alias` *(deprecated)* | type alias | **no effect** — alias rendering is controlled by Go aliases + options | [how-to]({{% relref "alias-rendering" %}}) | [reference]({{% relref "/maintainers/annotations/swagger-alias" %}}) |
| `swagger:allOf` | embedded field / struct | an `allOf` composition | [example]({{% relref "/tutorials/model-definitions#swaggerallof" %}}) | [reference]({{% relref "/maintainers/annotations/swagger-allof" %}}) |
| `swagger:default` | value / field doc | a default-value anchor | [example]({{% relref "/tutorials/examples-and-defaults#swaggerdefault" %}}) | [reference]({{% relref "/maintainers/annotations/swagger-default" %}}) |
| `swagger:default` *(deprecated)* | anywhere | **no effect** — use the `default:` keyword, or a `default:` response code | [how-to]({{% relref "/tutorials/examples-and-defaults" %}}) | [reference]({{% relref "/maintainers/annotations/swagger-default" %}}) |
| `swagger:description` | type / field / response doc | overrides the `description` (verbatim body with `\|`) | [how-to]({{% relref "overriding-titles-and-descriptions" %}}) | [reference]({{% relref "/maintainers/annotations/swagger-description" %}}) |
| `swagger:enum` | named type | an `enum` array (+ `x-go-enum-desc`) | [example]({{% relref "/tutorials/enumerations" %}}) | [reference]({{% relref "/maintainers/annotations/swagger-enum" %}}) |
| `swagger:file` | param / response field | `{type: file}` | [example]({{% relref "/tutorials/routes-and-operations#swaggerfile" %}}) | [reference]({{% relref "/maintainers/annotations/swagger-file" %}}) |
Expand Down
11 changes: 7 additions & 4 deletions docs/doc-site/maintainers/annotations/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ attach to:
- **Companion declarations**: `swagger:parameters`, `swagger:response`.
- **Local hints & overrides**: `swagger:ignore`, `swagger:omit`,
`swagger:name`, `swagger:title`, `swagger:description`, `swagger:type`,
`swagger:file`, `swagger:default`.
`swagger:file`.
- **Deprecated no-ops**, parsed and reported but without effect:
`swagger:alias`, `swagger:default`.

This section is the **author-first reference**. Each annotation has its
own page covering what it produces, where it goes, its EBNF-like
Expand Down Expand Up @@ -101,8 +103,9 @@ After the `swagger:<name>` head, an annotation may carry positional
arguments. The shapes:

- **No args**: `swagger:meta`, `swagger:ignore`, `swagger:enum`,
`swagger:allOf`, `swagger:file`, `swagger:default` — bare
annotation, the surrounding decl supplies the entity name.
`swagger:allOf`, `swagger:file` — bare annotation, the surrounding
decl supplies the entity name. `swagger:default` also accepts a bare
form, but its argument is optional and unread — it is deprecated.
- **One IDENT arg**: `swagger:model Pet`, `swagger:response
errorResponse`, `swagger:strfmt uuid`, `swagger:name fullName`,
`swagger:type integer`, `swagger:alias TimestampAlias` — the
Expand Down Expand Up @@ -142,7 +145,7 @@ contracts, and each annotation's own page for the detail.
| `swagger:additionalProperties` | — | ✅ (object schema) | — | — | — | — | — |
| `swagger:patternProperties` | — | ✅ (object schema) | — | — | — | — | — |
| `swagger:file` | — | — | — | — | — | — | — |
| `swagger:default` | — | — | — | — | — | — | — |
| `swagger:default` *(deprecated)* | — | — | — | — | — | — | — |

A blank cell means the keyword family is not legal in that context;
attempting to use it emits `CodeContextInvalid` and the keyword is
Expand Down
35 changes: 35 additions & 0 deletions docs/doc-site/maintainers/annotations/swagger-allof.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,41 @@ follow on the doc comment.
[Schema-context keywords]({{% relref "/maintainers/keywords/schema-validations-and-decorators#schema-decorators" %}}) on
the inline-object member (the second `allOf` element).

## Do not put other annotations beside it

`swagger:allOf` takes no arguments, and no other classifier annotation belongs in
an embedded field's doc comment. `swagger:strfmt` and `swagger:type` written
there are **ignored**, and codescan reports them under
`scan.ineffective-annotation`:

```go
type Wrong struct {
// swagger:allOf
// swagger:strfmt uuid ← ignored, and warned about
Token
}
```

The reason is that an embed contributes the shape of the type it embeds, and
what that shape is comes from **that type's own declaration** — never from the
site that embeds it. So the annotation belongs one level down:

```go
// Token is rendered as a formatted string wherever it appears.
//
// swagger:strfmt uuid
type Token [16]byte

type Right struct {
// swagger:allOf
Token
}
```

This is not specific to `allOf`: the same annotations are ignored on a plain
(uncomposed) embed too, and reported the same way. They are honoured on an
ordinary — non-embedded — field, which is what makes the mistake an easy one.

## Example

A struct embedding a `swagger:model` base with `swagger:allOf` on the embed
Expand Down
77 changes: 57 additions & 20 deletions docs/doc-site/maintainers/annotations/swagger-default.md
Original file line number Diff line number Diff line change
@@ -1,49 +1,86 @@
---
title: "swagger:default"
weight: 40
description: "Classifier hint marking a value declaration as a spec default anchor."
description: "Deprecated no-op — defaults are carried by the default: keyword, or a default response code."
---


{{% notice style="warning" %}}
**Deprecated.** `swagger:default` never emitted a `default` into the spec, in any
placement or form. It is now an empty sink that only raises a
`validate.deprecated` diagnostic. Use the
[`default:` keyword]({{% relref "/maintainers/keywords/schema-validations-and-decorators#default" %}}),
or a `default` response code in a route's `Responses:` body.
{{% /notice %}}

## Usage

```goish
// swagger:default
// swagger:default [ VALUE ]
```

## What it does

Marks the surrounding declaration as the spec's default value for the
corresponding shape.
Nothing. It is parsed, reported as deprecated, and ignored.

Previously it also **suppressed** the schema of a named basic type it was placed
on: the classifier claimed the target without writing it, so the declared type
published a typeless definition and every field referencing it emitted a typeless
property, silently. That is fixed — an annotated type now emits exactly what it
would emit unannotated.

## Why it was retired

Every place OpenAPI 2.0 admits a `default` is already served, so the annotation
had no meaning left to implement:

Used in narrow contexts where the scanner expects an explicit anchor for a
default. This annotation is **value-only** — there's no exported entity it
publishes; it's a classifier hint the scanner consumes during discovery.
| Where a default can appear | How to write it |
|---|---|
| Schema object — a model field, or a type declaration | [`default:` keyword]({{% relref "/maintainers/keywords/schema-validations-and-decorators#default" %}}) |
| Parameter object (non-body) | `default:` keyword |
| Items object | `default:` keyword |
| Header object | `default:` keyword |
| Responses object — an operation's default response | `default:` as the response code in a `Responses:` body |

The keyword's context set is exactly the list of OAS 2.0 objects that carry a
`default`; the response-code head closes the remainder.

## Where it goes

On a value declaration (`var`, `const`) or a struct field.
Anywhere it used to — the annotation is still recognised so existing source keeps
scanning. It has no effect wherever it appears.

## Grammar (EBNF)

```ebnf
DefaultClassifierBlock = ANN_DEFAULT , [ Title ] , [ Description ] ;
DefaultClassifierBlock = ANN_DEFAULT , [ VALUE ] , [ Title ] , [ Description ] ;
```

Takes no argument — an optional title/description may follow on the
doc comment.
The value argument is optional and unread. It used to be mandatory, which made
the bare form this page once documented a hard parse error.

## Supported keywords

None of its own. Most spec defaults are instead carried by the
[`default:` keyword]({{% relref "/maintainers/keywords/schema-validations-and-decorators#default" %}}) on the relevant
field; this annotation has a narrow surface and is not commonly authored
directly.
None.

## Example

`swagger:default` is value-only: it produces no definition, so there is no
emitted spec to render. The source below shows the narrow classifier-hint
form — in practice most defaults come from the
[`default:` keyword]({{% relref "/maintainers/keywords/schema-validations-and-decorators#default" %}}) on a field.
Replace it with the keyword:

```go
// Port is the listen port.
//
// swagger:model Port
// default: 8080
type Port int
```

{{< code file="concepts/examples/examples.go" region="swaggerdefault" lang="go" >}}
For an operation's default response, use the response code:

```go
// swagger:route GET /things things listThings
//
// Responses:
// 200: thingList
// default: genericError
```
12 changes: 11 additions & 1 deletion docs/doc-site/maintainers/annotations/swagger-enum.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ description: "Marks a named type as an enum and collects its const values."
// swagger:enum [ IDENT_NAME ]
```

{{% notice style="note" %}}
Not to be confused with the [`enum:` keyword]({{% relref "/maintainers/keywords/schema-validations-and-decorators#enum" %}}),
which produces the same spec keyword from the opposite direction: it takes the
members you write literally, typed from the schema it sits on, whereas this
annotation collects them from a Go `const` block, typed from the declared Go
type. Side-by-side in the [enumerations tutorial]({{% relref "/tutorials/enumerations" %}}).
{{% /notice %}}

## What it does

Marks a named type over a string, integer, number or boolean as an enum
Expand All @@ -27,8 +35,10 @@ A type declared over another named type keeps that type's format
(`type Kind strfmt.UUID` stays `format: uuid`).

Two shapes do not work: an alias to a basic type cannot host an enum (the
type-checker erases the alias, leaving nothing to collect), and a `rune`
type-checker erases the alias, leaving nothing to collect — this raises a
`parse.invalid-enum-option` warning suggesting a named type), and a `rune`
or `byte` enum emits integers, which is what those types are on the wire.
An alias to a *named* enum type is fine, and is not warned about.
See [Enumerations]({{% relref "/tutorials/enumerations" %}}).

- **Without `swagger:model`** (the default): the values are applied
Expand Down
6 changes: 6 additions & 0 deletions docs/doc-site/maintainers/annotations/swagger-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ weight: 60
description: "Marks a parameter or response body as a binary file (`{type: file}`)."
---

{{% notice style="note" %}}
**Prefer [`swagger:type file`]({{% relref "swagger-type" %}}).** The two are exact
synonyms — same output, same location gate. `swagger:file` is expected to be
deprecated as an extraneous annotation; it is not deprecated yet and still works.
{{% /notice %}}

## Usage

```goish
Expand Down
12 changes: 10 additions & 2 deletions docs/doc-site/maintainers/annotations/swagger-type.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,13 @@ field-level inline form above is the behaviour *without* `swagger:model`.
- The `array` argument is **deprecated** — use `inline`, or `[]T` for an
explicit element type. It still works, with a `validate.deprecated`
warning.
- `file` as an argument is rejected with a diagnostic — use
[`swagger:file`]({{% relref "swagger-file" %}}).
- `file` used to be rejected as an argument. It is now accepted, and is the
**preferred** spelling: `file` is an OAS v2 type name like any other, so the
annotation that names types names it too. It is a synonym for
[`swagger:file`]({{% relref "swagger-file" %}}), which is expected to be
deprecated as an extraneous annotation.

`file` is legal in exactly two places — a `formData` parameter and a response
body. Both spellings pass through the same location gate, so neither can put
`file` anywhere OAS 2.0 forbids it; elsewhere the override is refused with a
diagnostic and the Go type stands.
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ more idiomatic — it picks up the constant names + godoc and produces
`SkipEnumDescriptions: true` to keep the const→value mapping on `x-go-enum-desc` only,
out of the description.)

Do not confuse the two: the **annotation** collects members from a Go `const` block
and types them from the declared Go type; the **keyword** takes the members you write
and types them from the schema it sits on. Side-by-side comparison in the
[enumerations tutorial]({{% relref "/tutorials/enumerations" %}}).

### `required`

Marks a field as required. Boolean.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,34 @@ embeds should compose; reach for the option when composition is your house style
for every plain embed.
{{% /notice %}}

## Annotate the embedded type, not the embed

A classifier annotation in an **embedded field's** doc comment does nothing.
`swagger:strfmt` and `swagger:type` written there are ignored — codescan reports
them under `scan.ineffective-annotation` rather than dropping them quietly:

```go
type Wrong struct {
// swagger:strfmt uuid ← ignored, and warned about
Token
}
```

An embed contributes the shape of the type it embeds, and what that shape is
comes from that type's own declaration. Put the annotation there and every embed
of it composes the same way:

```go
// swagger:strfmt uuid
type Token [16]byte
```

The catch is that both annotations *are* honoured on an ordinary field, so the
same line means something one field down and nothing on an embed. Only
`swagger:allOf`, [`swagger:omit`]({{% relref "/maintainers/annotations/swagger-omit" %}}),
`swagger:name`, `swagger:ignore` and a `required:` inheritance hint act on an
embed itself — everything else describes the embedded type and belongs with it.

## When an override cannot be composed

Composition has one limit worth knowing. Inlining an embed **resolves** an
Expand Down
31 changes: 31 additions & 0 deletions docs/doc-site/tutorials/enumerations.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,37 @@ that pair into an `enum` array on every schema, parameter and header the type
reaches. This page covers what the scanner accepts on the value side, what
decides the emitted `type` / `format`, and the two shapes that do not work.

{{% notice style="note" title="`swagger:enum` and `enum:` are two different things" %}}
They produce the same spec keyword from opposite directions, and the names are
close enough to trip over:

| | `swagger:enum` — an **annotation** | `enum:` — a **keyword** |
|---|---|---|
| Where | on the type declaration | inside any annotation block, on a field, parameter, header or declaration |
| Members come from | the Go `const` block of that type, read from the type-checker | the literal list you write after the colon |
| Type / format | the **declared Go type** | the schema the keyword sits on |
| Use it when | the values already exist as Go constants | there is no const block, or the members are not Go values at all |

```go
// swagger:enum Kind ← annotation: members are collected from the consts
type Kind string
const (
KindA Kind = "a"
KindB Kind = "b"
)

type Filter struct {
// enum: asc, desc ← keyword: members are what you wrote
Order string `json:"order"`
}
```

The annotation is the better tool whenever the constants exist: it stays in
sync with the code, carries each member's doc comment into `x-go-enum-desc`,
and cannot drift from the Go values. The keyword is the escape hatch for
everything else.
{{% /notice %}}

Every Go snippet below comes from the test-covered
[`docs/examples/concepts/enums`](https://github.com/go-openapi/codescan/tree/master/docs/examples/concepts/enums)
package, and every JSON pane is a golden file a test regenerates.
Expand Down
18 changes: 12 additions & 6 deletions docs/doc-site/tutorials/examples-and-defaults.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,20 @@ is a number, `false` a boolean, `auto` a string.
{{< example go="concepts/examples/examples.go" goregion="default"
json="concepts/examples/testdata/default.json" jsonlabel="#/definitions/Settings" >}}

## swagger:default
## swagger:default (deprecated)

`swagger:default` is a narrow, value-only classifier hint placed on a `var` or
`const`. It does not publish a spec entity of its own — it has no standalone
output — so most spec defaults are carried by the `default:` keyword above
rather than this annotation.
{{% notice style="warning" %}}
`swagger:default` never emitted a `default` into the spec. It is now an inert
sink that raises a `validate.deprecated` diagnostic. Use the `default:` keyword
above.
{{% /notice %}}

{{< code file="concepts/examples/examples.go" lang="go" region="swaggerdefault" >}}
The keyword covers every place OpenAPI 2.0 admits a default value: a model field,
a non-body parameter, a header, and array items. The one remaining sense of
"default" — an operation's *default response* — is not a value at all; it is
written as a response code in a route's `responses:` body:

{{< code file="concepts/routes/routes.go" lang="go" region="route" >}}

## On a defined-type field

Expand Down
Loading
Loading