Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions docs/doc-site/tutorials/model-definitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ the JSON-Schema `type` / `format`. Well-known standard-library types resolve
automatically — a `time.Time` field, for instance, is published as
`{type: string, format: date-time}`.

UUIDs are recognised two different ways, and both publish
`{type: string, format: uuid}`:

- **by type identity** — the standard-library `uuid.UUID` introduced in Go 1.27,
matched on its import path and name, so nothing else can be mistaken for it;
- **by type name** — any *other* type named `UUID` (case-insensitively) that
marshals as text, which covers `github.com/google/uuid`, `gofrs/uuid`,
`strfmt.UUID` and the like.

The name-based rule is a heuristic and stays available whichever Go version you
build with. Either way an explicit [`swagger:strfmt`](#swaggerstrfmt) on the type
wins, so you can always overrule the recognition.

{{< example go="concepts/models/models.go" goregion="model"
json="concepts/models/testdata/model.json" jsonlabel="#/definitions/Pet" >}}

Expand Down
10 changes: 10 additions & 0 deletions fixtures/goparsing/go127/uuid/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

// Package uuidmodels witnesses the identity-based recognizer for the go1.27 stdlib uuid package.
//
// This file is deliberately UNTAGGED while model.go carries `//go:build go1.27`: under an older
// toolchain `import "uuid"` does not compile, and a package left with no buildable files at all
// fails `go build ./...` with "build constraints exclude all Go files". This doc file keeps the
// package loadable while contributing nothing to the emitted spec.
package uuidmodels
67 changes: 67 additions & 0 deletions fixtures/goparsing/go127/uuid/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

//go:build go1.27

package uuidmodels

import "uuid"

// Order carries the stdlib uuid.UUID in every position the schema builder can reach it from.
//
// swagger:model Order
type Order struct {
// ID is a plain field.
ID uuid.UUID `json:"id"`

// PtrID is a pointer, peeled before recognition.
PtrID *uuid.UUID `json:"ptrId"`

// IDs exercises the items chain.
IDs []uuid.UUID `json:"ids"`

// ByName exercises a map value; the key is a plain string.
ByName map[string]uuid.UUID `json:"byName"`

// Keyed exercises uuid.UUID as a map KEY: it marshals to a JSON object because the key is a
// TextMarshaler, and the key type itself is not rendered in Swagger 2.0.
Keyed map[uuid.UUID]string `json:"keyed"`

// Named refs the named type declared below.
Named OrderID `json:"named"`

// Overridden proves the explicit classifier still beats the recognizer.
//
// swagger:strfmt date
Overridden uuid.UUID `json:"overridden"`
}

// OrderID is a named type whose underlying type is the stdlib uuid.UUID.
//
// swagger:model OrderID
type OrderID uuid.UUID

// AliasID is an ALIAS of the stdlib uuid.UUID: it dissolves at use sites.
type AliasID = uuid.UUID

// Tagged uses the alias.
//
// swagger:model Tagged
type Tagged struct {
// Alias is declared through the alias above.
Alias AliasID `json:"alias"`
}

// Embedder embeds the stdlib uuid.UUID.
//
// Witness for the embedded arm, which the fuzzy name heuristic never reached (it is caller-gated to
// buildFromTextMarshal). Note the promoted MarshalText makes encoding/json render the WHOLE struct
// as a bare string, dropping Name from the wire — see quirks-open.md Q33.
//
// swagger:model Embedder
type Embedder struct {
uuid.UUID

// Name is NOT on the wire, whatever the emitted schema says.
Name string `json:"name"`
}
92 changes: 92 additions & 0 deletions fixtures/integration/golden/go127_uuid_spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
{
"swagger": "2.0",
"paths": {},
"definitions": {
"Embedder": {
"description": "Witness for the embedded arm, which the fuzzy name heuristic never reached (it is caller-gated to\nbuildFromTextMarshal). Note the promoted MarshalText makes encoding/json render the WHOLE struct\nas a bare string, dropping Name from the wire — see quirks-open.md Q33.",
"type": "object",
"title": "Embedder embeds the stdlib uuid.UUID.",
"properties": {
"name": {
"description": "Name is NOT on the wire, whatever the emitted schema says.",
"type": "string",
"x-go-name": "Name"
}
},
"x-go-package": "github.com/go-openapi/codescan/fixtures/goparsing/go127/uuid"
},
"Order": {
"type": "object",
"title": "Order carries the stdlib uuid.UUID in every position the schema builder can reach it from.",
"properties": {
"byName": {
"description": "ByName exercises a map value; the key is a plain string.",
"type": "object",
"additionalProperties": {
"type": "string",
"format": "uuid"
},
"x-go-name": "ByName"
},
"id": {
"description": "ID is a plain field.",
"type": "string",
"format": "uuid",
"x-go-name": "ID"
},
"ids": {
"description": "IDs exercises the items chain.",
"type": "array",
"items": {
"type": "string",
"format": "uuid"
},
"x-go-name": "IDs"
},
"keyed": {
"description": "Keyed exercises uuid.UUID as a map KEY: it marshals to a JSON object because the key is a\nTextMarshaler, and the key type itself is not rendered in Swagger 2.0.",
"type": "object",
"additionalProperties": {
"type": "string"
},
"x-go-name": "Keyed"
},
"named": {
"$ref": "#/definitions/OrderID"
},
"overridden": {
"description": "Overridden proves the explicit classifier still beats the recognizer.",
"type": "string",
"format": "date",
"x-go-name": "Overridden"
},
"ptrId": {
"description": "PtrID is a pointer, peeled before recognition.",
"type": "string",
"format": "uuid",
"x-go-name": "PtrID"
}
},
"x-go-package": "github.com/go-openapi/codescan/fixtures/goparsing/go127/uuid"
},
"OrderID": {
"type": "string",
"format": "uuid",
"title": "OrderID is a named type whose underlying type is the stdlib uuid.UUID.",
"x-go-package": "github.com/go-openapi/codescan/fixtures/goparsing/go127/uuid"
},
"Tagged": {
"type": "object",
"title": "Tagged uses the alias.",
"properties": {
"alias": {
"description": "Alias is declared through the alias above.",
"type": "string",
"format": "uuid",
"x-go-name": "Alias"
}
},
"x-go-package": "github.com/go-openapi/codescan/fixtures/goparsing/go127/uuid"
}
}
}
13 changes: 13 additions & 0 deletions internal/builders/resolvers/assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,19 @@ func IsStdTime(o *types.TypeName) bool {
return o.Pkg() != nil && o.Pkg().Name() == "time" && o.Name() == "Time"
}

// IsStdUUID reports whether o is the go1.27 stdlib [uuid.UUID].
//
// Identity-based, so it never misfires on the many third-party types also named UUID
// (github.com/google/uuid, gofrs, strfmt, …) — those keep going through the fuzzy
// name heuristic in the schema builder.
//
// Deliberately NOT behind a go1.27 build tag: codescan compares types harvested from
// *scanned* code, never imports uuid itself, and ships as a binary that may be built by
// an older toolchain than the module it scans.
func IsStdUUID(o *types.TypeName) bool {
return o.Pkg() != nil && o.Pkg().Path() == "uuid" && o.Name() == "UUID"
}

func IsStdError(o *types.TypeName) bool {
return o.Pkg() == nil && o.Name() == "error"
}
Expand Down
60 changes: 60 additions & 0 deletions internal/builders/resolvers/assertions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

package resolvers

import (
"go/token"
"go/types"
"testing"

"github.com/go-openapi/testify/v2/assert"
)

// TestIsStdUUID covers the go1.27 stdlib uuid.UUID predicate.
//
// Deliberately synthesizes the *types.TypeName instead of scanning a fixture, so the predicate stays
// covered on every supported toolchain — including the ones where `import "uuid"` does not compile.
// The end-to-end wiring is witnessed by the go1.27-tagged integration test instead.
func TestIsStdUUID(t *testing.T) {
// stdlib packages have a bare import path; uuid.UUID is [16]byte.
stdUUID := func() *types.TypeName {
pkg := types.NewPackage("uuid", "uuid")
return types.NewTypeName(token.NoPos, pkg, "UUID", types.NewArray(types.Typ[types.Byte], 16))
}

named := func(path, name, typeName string) *types.TypeName {
pkg := types.NewPackage(path, name)
return types.NewTypeName(token.NoPos, pkg, typeName, types.NewArray(types.Typ[types.Byte], 16))
}

t.Run("the go1.27 stdlib uuid.UUID is recognized", func(t *testing.T) {
assert.TrueT(t, IsStdUUID(stdUUID()))
})

t.Run("third-party UUID types are NOT recognized by identity", func(t *testing.T) {
// These stay on the fuzzy name heuristic in the schema builder; identity must not claim them.
assert.FalseT(t, IsStdUUID(named("github.com/google/uuid", "uuid", "UUID")))
assert.FalseT(t, IsStdUUID(named("github.com/gofrs/uuid/v5", "uuid", "UUID")))
assert.FalseT(t, IsStdUUID(named("github.com/go-openapi/strfmt", "strfmt", "UUID")))
})

t.Run("a user package merely named uuid is NOT recognized", func(t *testing.T) {
// Package *name* is uuid, but the import path carries the module prefix — only the stdlib
// gets a bare path. This is why the predicate tests Path(), not Name().
assert.FalseT(t, IsStdUUID(named("example.com/mymod/internal/uuid", "uuid", "UUID")))
})

t.Run("another type in the stdlib uuid package is NOT recognized", func(t *testing.T) {
assert.FalseT(t, IsStdUUID(named("uuid", "uuid", "Version")))
})

t.Run("the match is case-sensitive", func(t *testing.T) {
assert.FalseT(t, IsStdUUID(named("uuid", "uuid", "Uuid")))
})

t.Run("a builtin (no package) does not panic", func(t *testing.T) {
builtin := types.NewTypeName(token.NoPos, nil, "error", nil)
assert.FalseT(t, IsStdUUID(builtin))
})
}
55 changes: 43 additions & 12 deletions internal/builders/schema/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ trade-offs, and known quirks live here.
- [§build-entry](#build-entry) — `Build` modes and dispatch entry points
- [§dispatch-table](#dispatch-table) — `buildNamedType`'s underlying-shape table
- [§dissolve-named](#dissolve-named) — when a named type is unwrapped instead of `$ref`'d
- [§special-types](#special-types) — `applyStdlibSpecials`, `applySpecialType`, UUID heuristic
- [§special-types](#special-types) — `applyStdlibSpecials`, `applySpecialType`, the two UUID recognizers
- [§textmarshal-order](#textmarshal-order) — `buildFromTextMarshal` precedence
- [§aliases](#aliases) — `TransparentAliases` vs `RefAliases` vs default-expand
- [§discovery](#discovery) — `Models` / `ExtraModels` / `discovered`, dedup layers
Expand Down Expand Up @@ -137,7 +137,7 @@ Fixture: `fixtures/enhancements/generic-instantiation/`.

---

## <a id="special-types"></a>§special-types — `applyStdlibSpecials`, `applySpecialType`, UUID heuristic
## <a id="special-types"></a>§special-types — `applyStdlibSpecials`, `applySpecialType`, the two UUID recognizers

Three layers, all in `special_types.go`:

Expand All @@ -147,15 +147,46 @@ Three layers, all in `special_types.go`:
`recognizeUUID` which is **fuzzy** (case-insensitive name match).

- **`applyStdlibSpecials(obj, target)`** — the canonical safe set
`{recognizeAny, recognizeTime, recognizeError, recognizeRawMessage}`.
All four are identity-based and cannot misfire on user types,
so this helper is **called uniformly at every site** that handles
a `*types.TypeName`.

- **`recognizeUUID`** — opt-in via `applySpecialType`'s variadic.
Currently used **only by `buildFromTextMarshal`** because the
upstream `IsTextMarshaler` gate guarantees the type renders as
text, making the fuzzy name match safe.
`{recognizeAny, recognizeTime, recognizeError, recognizeRawMessage,
recognizeStdUUID}`. All five are identity-based and cannot misfire
on user types, so this helper is **called uniformly at every site**
that handles a `*types.TypeName`.

The two UUID recognizers are a **certain/guessed pair**, and the
distinction is the whole reason both exist:

- **`recognizeStdUUID`** — identity match on the go1.27 stdlib
`uuid.UUID` (`resolvers.IsStdUUID`: import path `uuid`, name `UUID`).
Certain, therefore in the safe set. It is **not** behind a
`//go:build go1.27` tag, on purpose: codescan compares types
harvested from *scanned* code and never imports `uuid` itself, so
tagging it would mean a codescan built by an older toolchain fails
to recognize a go1.27 user type — the inverted matrix. Build tags
live on the fixture and the integration test, which genuinely cannot
compile before go1.27; the predicate keeps toolchain-independent
coverage via `TestIsStdUUID`.

- **`recognizeUUID`** — fuzzy, case-insensitive name match, the
fallback for `github.com/google/uuid`, `gofrs/uuid`, `strfmt.UUID`
and every other third-party type named UUID. Opt-in via
`applySpecialType`'s variadic and used **only by
`buildFromTextMarshal`**, because the upstream `IsTextMarshaler`
gate guarantees the type renders as text, making the name match
safe. `buildFromTextMarshal` orders identity **before** fuzzy so
the certain match wins.

Neither beats an explicit `swagger:strfmt` / `swagger:type` — the
classifier runs first, see [§textmarshal-order](#textmarshal-order).

**Not reached by either: a struct embed.** `uuid.UUID` has an array
underlying type, so `buildNamedEmbedded` falls to its `default` arm
(only the *interface* arm consults `applyStdlibSpecials`) and skips
the embed with a `CodeUnsupportedGoType` warning. That asymmetry is
long-standing and uuid-agnostic — any `strfmt.UUID` / `time.Time`
embed behaves the same, and Go itself renders such a struct as a bare
string via the promoted `MarshalText`. Tracked as Q33; the identity
recognizer deliberately does not change it. Witnessed by
`TestStdlibUUID`'s embed sub-test.

The seven call sites of `applyStdlibSpecials` (`buildFromDecl`,
`buildDeclAlias` RHS, `buildAlias`, `buildNamedType`,
Expand All @@ -177,7 +208,7 @@ The function is entered from `buildFromType`'s shortcut
2. route aliases through `buildAlias` (honour `TransparentAliases` / `RefAliases`)
3. type-assert to `*types.Named` (fallback: `{string, ""}`)
4. **classifier (`swagger:strfmt`) — explicit user intent wins**
5. **stdlib trio via `applySpecialType(recognizeError, recognizeTime, recognizeRawMessage, recognizeUUID)`**
5. **stdlib recognizers via `applySpecialType(recognizeError, recognizeTime, recognizeRawMessage, recognizeStdUUID, recognizeUUID)`** — identity before fuzzy
6. `PkgForType`-miss bail (gates only the generic fallback below)
7. **generic fallback** — `{string, ""}` + `x-go-type: pkg.Name`

Expand Down
Loading
Loading