-
Notifications
You must be signed in to change notification settings - Fork 0
Schemas
Go types become JSON Schema 2020-12 — the dialect OpenAPI 3.1 uses — by reflection over the struct.
type CreateUserRequest struct { Email string `json:"email" validate:"required,email"` Name string `json:"name" validate:"required,min=2,max=100"` Age int `json:"age" validate:"gte=18,lte=120"` Role string `json:"role" validate:"required,oneof=admin user guest"` Website *string `json:"website"` Tags []string `json:"tags"` Address Address `json:"address"` }
becomes
{
"$ref": "#/components/schemas/CreateUserRequest"
}with, in components/schemas:
{
"type": "object",
"required": ["email", "name", "role"],
"properties": {
"email": {"type": "string"},
"name": {"type": "string", "minLength": 2, "maxLength": 100},
"age": {"type": "integer", "minimum": 18, "maximum": 120},
"role": {"type": "string", "enum": ["admin", "user", "guest"]},
"website": {"type": "string", "nullable": true},
"tags": {"type": "array", "items": {"type": "string"}},
"address": {"$ref": "#/components/schemas/Address"}
}
}| From the Go type | Into the schema |
|---|---|
string, int, float64, bool
|
type, and format where it applies |
| struct | an object, hoisted into components/schemas and referenced by $ref
|
| slice / array |
type: array with items
|
| pointer | the pointee's schema, nullable: true
|
| map | an object |
json:"name" |
the property name |
json:"-" |
omitted |
| Tag | Schema |
|---|---|
required |
added to the object's required list |
min / max on a string |
minLength / maxLength
|
gte / lte on a number |
minimum / maximum
|
oneof=a b c |
enum |
So the documented constraints are the ones actually enforced — the same tags the validation extension reads. A constraint cannot drift out of the document, because there is only one declaration.
rextension.Scalar(CreateUserRequest{}) // $ref rextension.OneOf(CardPayment{}, BankTransfer{}) // oneOf: [$ref, $ref] rextension.AnyOf(EmailContact{}, SMSContact{}) // anyOf rextension.AllOf(BaseEvent{}, OrderPayload{}) // allOf
These map onto the OpenAPI keywords of the same names, so the document says exactly what the validator enforces.
Every named struct is registered in components/schemas and referenced by
$ref, so a type used by twenty routes appears once. Nested structs are hoisted
recursively.
The component key is the Go type name. Two types with the same name in different packages collide — rename one, or wrap it:
type UserResponse struct{ users.User } // distinct name in the document
func (r *GetUser) Responses() map[int]rextension.BodySchema { return map[int]rextension.BodySchema{ 200: rextension.Scalar(UserResponse{}), 404: rextension.Scalar(rextension.Problem{}), 500: rextension.Scalar(rextension.Problem{}), } }
rextension.Problem is the shape of every framework and extension error, so
documenting it once per status makes the document match reality. A helper keeps
it terse:
func problems(codes ...int) map[int]rextension.BodySchema { m := make(map[int]rextension.BodySchema, len(codes)) for _, c := range codes { m[c] = rextension.Scalar(rextension.Problem{}) } return m }
g := openapi.NewSchemaGenerator() s := g.Generate(CreateUserRequest{}) components := g.Components()
Useful in a test that asserts a type's schema, or for generating schemas outside the extension.
-
No
formatinference from validate tags.validate:"email"contributesrequired-style information but does not emitformat: email. Add it in the description if it matters to your consumers. -
Interfaces and
anyproduce an untyped object. The generator reflects on a static type; if a field isinterface{}, there is nothing to reflect on. -
Recursive types are handled by the
$refhoisting — a type referring to itself references its own component.
The generator used to call RequestBody() and Responses() through
reflect.MethodByName, on the reasoning that "any route implementing
validation.ValidatableRoute works automatically". Reflection was really a
workaround for the two extensions having no shared type — and it came with an
unchecked .([]interface{}) assertion that panicked inside document
generation on any unexpected slice type.
The contract now lives in rextension, both extensions assert the same named
interface, and the reflection is gone.
Ecosystem