Schema Generation
Open-swag-go reads your Go structs and turns them into OpenAPI 3.x schema definitions. Struct tags control field names, descriptions, examples, formats, and validation rules — so the generated spec stays in sync with your actual types.
How It Works
When you pass a Go struct as the Schema field on a RequestBody, Response, or Parameter, open-swag-go inspects the struct's fields and tags at runtime. Each exported field becomes a property in the resulting OpenAPI schema object.
type CreateUserRequest struct {
Name string `json:"name" description:"Full name of the user" example:"Alice Smith"`
Email string `json:"email" description:"Email address" example:"alice@example.com"`
Age int `json:"age" description:"Age in years" example:"30"`
}This struct produces the following OpenAPI schema:
type: object
properties:
name:
type: string
description: Full name of the user
example: Alice Smith
email:
type: string
description: Email address
example: alice@example.com
age:
type: integer
description: Age in years
example: 30Supported Struct Tags
json
Controls the property name in the generated schema. Follows standard encoding/json conventions.
type Product struct {
ID int64 `json:"id"`
Name string `json:"name"`
SKU string `json:"sku"`
IsActive bool `json:"is_active"`
Internal string `json:"-"` // omitted from schema
CreatedAt string `json:"created_at,omitempty"`
}json:"field_name"— sets the property namejson:"-"— excludes the field from the schemajson:"name,omitempty"— marks the field as optional
swagger
Provides OpenAPI-specific overrides: marking a field as required, and setting its format, description or example. Values are comma-separated.
type Order struct {
ID int64 `json:"id" swagger:"required"`
Email string `json:"email" swagger:"required,format=email"`
Status string `json:"status" swagger:"required" enum:"pending,processing,shipped,delivered"`
Notes *string `json:"notes" swagger:"description=Optional delivery notes"` // pointer = nullable
}| Value | Effect |
|---|---|
required | Adds the field to the schema's required array |
format=... | Sets format (same as the format tag) |
description=... | Sets description (same as the description tag) |
example=... | Sets example, always as a string (use the example tag for type coercion) |
These four are the only values the swagger tag understands; anything else is ignored. In particular enum=, nullable, readOnly, writeOnly and deprecated are not supported. Use a pointer type for nullable fields, and the enum tag or validate:"oneof=..." for enums.
description
Sets the description field on the schema property.
type Pagination struct {
Page int `json:"page" description:"Current page number (1-based)"`
Limit int `json:"limit" description:"Maximum items per page"`
Total int `json:"total" description:"Total number of matching items"`
}example
Provides an example value shown in the generated docs.
type Address struct {
Street string `json:"street" example:"123 Main St"`
City string `json:"city" example:"Springfield"`
State string `json:"state" example:"IL"`
ZipCode string `json:"zip_code" example:"62701"`
}format
Sets the OpenAPI format hint for the property. Common formats include date-time, email, uri, uuid, ipv4, and int64.
type Event struct {
ID string `json:"id" format:"uuid"`
Email string `json:"email" format:"email"`
StartTime string `json:"start_time" format:"date-time"`
Website string `json:"website" format:"uri"`
}validate
Maps go-playground/validator rules to OpenAPI schema constraints (v1.3.0+; before that only required was read). The constraint depends on the field type.
type CreateProductRequest struct {
Name string `json:"name" validate:"required,min=1,max=200"`
Price float64 `json:"price" validate:"required,gt=0"`
Quantity int `json:"quantity" validate:"gte=0,lte=10000"`
SKU string `json:"sku" validate:"required,len=10"`
Color string `json:"color" validate:"oneof=red green 'dark blue'"`
Contact string `json:"contact" validate:"required,email"`
Tags []string `json:"tags" validate:"min=1,max=10,dive,min=2,max=20"`
}| Rule | string | number | slice / array |
|---|---|---|---|
required | Adds to required | Adds to required | Adds to required |
min=N / gte=N | minLength: N | minimum: N | minItems: N |
max=N / lte=N | maxLength: N | maximum: N | maxItems: N |
gt=N | minLength: N+1 | minimum: N (approximation) | minItems: N+1 |
lt=N | maxLength: N-1 | maximum: N (approximation) | maxItems: N-1 |
len=N | minLength and maxLength set to N | minimum and maximum set to N | minItems and maxItems set to N |
oneof=a b c | enum: [a, b, c] | enum with typed values | — |
email | format: email | — | — |
uuid, uuid3, uuid4, uuid5 | format: uuid | — | — |
url, uri | format: uri | — | — |
ipv4, ipv6, hostname | format: ipv4 / ipv6 / hostname | — | — |
dive,<rules> | — | — | <rules> apply to items |
Notes:
gtandlton numbers are emitted as the inclusiveminimum/maximum;exclusiveMinimumandexclusiveMaximumare never written. For string lengths and item counts, which are integers, the exclusive bound is exact (N+1/N-1).oneofvalues are separated by spaces; wrap a value containing spaces in single quotes (oneof='dark blue' red). Values are converted to the field type, so anintfield gets integer enum values. If the field is a pointer (nullable),nullis appended to the enum.- Format rules only set
formatwhen it is not already set by aformattag. - Only a bare
requiredmarks a field required. Conditional rules such asrequired_if,required_withandrequired_withoutdo not (v1.3.0+). - Rules containing
|(alternatives), rules with parameters that do not parse as numbers, and unknown rules such asuniqueare ignored: the schema stays less strict rather than wrong.
enum (v1.3.0+)
Sets explicit enum values, comma-separated. Values are converted to the field type. When both enum and validate:"oneof=..." are present, enum wins.
type Task struct {
Status string `json:"status" enum:"todo,in_progress,done"`
Priority int `json:"priority" enum:"1,2,3"` // integer enum: [1, 2, 3]
}binding
Works like validate but is recognized from frameworks like Gin. Open-swag-go treats binding tags the same as validate.
type LoginRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Password string `json:"password" binding:"required,min=8"`
}Type Mapping
Open-swag-go maps Go types to OpenAPI types automatically:
| Go Type | OpenAPI Type | OpenAPI Format |
|---|---|---|
string | string | — |
int, int8, int16 | integer | — |
int32 | integer | int32 |
int64 | integer | int64 |
float32 | number | float |
float64 | number | double |
bool | boolean | — |
[]T | array | items: T |
struct | object (named structs in bodies: $ref, v1.3.0+) | — |
map[string]T | object | additionalProperties: T |
time.Time | string | date-time |
time.Duration | string | duration (v1.1.0+) |
[]byte | string | byte (v1.1.0+) |
*T | [T, "null"] (v1.3.0+) | same as T |
uuid.UUID | string | uuid |
interface{}/any | — (empty schema) | accepts any type (v1.1.0+) |
Embedded Structs (v1.1.0+)
Embedded (anonymous) struct fields are automatically promoted into the parent schema, matching Go's field promotion behavior. This is useful for shared base types like timestamps or audit fields.
type Timestamps struct {
CreatedAt time.Time `json:"created_at" description:"When the record was created"`
UpdatedAt time.Time `json:"updated_at" description:"When the record was last updated"`
}
type User struct {
Timestamps // Embedded — fields promoted into User
ID int64 `json:"id"`
Name string `json:"name"`
}The generated schema for User includes created_at and updated_at as top-level properties alongside id and name.
The promotion rules follow encoding/json (v1.3.0+):
type base struct { // unexported type name
ID string `json:"id"`
CreatedAt time.Time `json:"created_at"`
}
type Order struct {
base // promoted: id and created_at become Order properties
ID string `json:"id" description:"Order number"` // shadows base.ID
}
type Invoice struct {
base `json:"base"` // json name given: nested object property "base", not flattened
Total float64 `json:"total"`
}- An embedded struct whose type is unexported (
base) is still promoted. Before v1.3.0 its fields were silently dropped. - A field declared on the outer struct wins over a promoted field with the same JSON name, regardless of field order.
- A json tag with a name on the embedded field turns it into an ordinary nested property instead of flattening it.
Map Types (v1.1.0+)
map[K]V types now correctly generate {"type":"object","additionalProperties":{...}} where the value schema describes V.
type Config struct {
Labels map[string]string `json:"labels" description:"Key-value labels"`
Metadata map[string]any `json:"metadata" description:"Arbitrary metadata"`
}type: object
properties:
labels:
type: object
additionalProperties:
type: string
metadata:
type: object
additionalProperties: {}Interface and Any Types (v1.1.0+)
Fields typed as interface{} or any now produce an empty schema {} (which accepts any JSON value) instead of incorrectly generating {"type":"object"}.
type Event struct {
Type string `json:"type" description:"Event type"`
Payload any `json:"payload" description:"Event-specific data"`
}Omitempty and Required Interaction (v1.1.0+)
Fields with json:",omitempty" are never marked as required in the generated schema, even if validate:"required" or swagger:"required" is set. The omitempty tag takes precedence because it signals that the field is optional at the JSON serialization level.
type UpdateUserRequest struct {
Name string `json:"name" validate:"required"` // required ✓
Email string `json:"email,omitempty" validate:"required"` // NOT required (omitempty wins)
Phone string `json:"phone,omitempty"` // NOT required
}Composition Keywords (v1.1.0+)
The schema.Schema type has fields for the allOf, oneOf and anyOf composition keywords and for readOnly, writeOnly, deprecated and uniqueItems, and they are carried through to the spec. The struct reflector never sets them from tags, so they only appear when you build a schema by hand. The one generated exception: since v1.3.0 a nullable reference to a schema component is written as oneOf: [{$ref}, {type: "null"}]. Array length constraints do come from tags:
| Annotation | Struct Tag | Effect |
|---|---|---|
minItems | validate:"min=N" on slices | Minimum array length |
maxItems | validate:"max=N" on slices | Maximum array length |
type User struct {
Tags []string `json:"tags" validate:"min=1,max=10"` // minItems: 1, maxItems: 10
}Special Type Handling (v1.1.0+)
Certain Go types receive special format annotations:
| Go Type | Generated Format | Description |
|---|---|---|
time.Duration | duration | ISO 8601 duration |
[]byte | byte | Base64-encoded binary data |
uuid.UUID | uuid | UUID string |
Practical Examples
Tagged struct with validation
type CreateOrderRequest struct {
CustomerID string `json:"customer_id" swagger:"required" description:"Customer UUID" format:"uuid" example:"550e8400-e29b-41d4-a716-446655440000"`
Items []OrderItem `json:"items" swagger:"required" description:"Line items" validate:"required,min=1"`
Notes string `json:"notes" description:"Optional notes" example:"Leave at front door"`
Priority string `json:"priority" enum:"low,normal,high" description:"Order priority" example:"normal"`
}
type OrderItem struct {
ProductID string `json:"product_id" swagger:"required" description:"Product UUID" format:"uuid"`
Quantity int `json:"quantity" swagger:"required" description:"Number of units" validate:"gte=1,lte=100" example:"2"`
}Using schemas in endpoints
Pass your tagged structs as Schema values on request bodies and responses:
openswag.Endpoint{
Method: "POST",
Path: "/orders",
Summary: "Create an order",
Tags: []string{"Orders"},
RequestBody: &openswag.RequestBody{
Description: "Order to create",
ContentType: "application/json",
Required: true,
Schema: CreateOrderRequest{},
},
Responses: openswag.Responses{
201: {Description: "Order created",
Schema: OrderResponse{},
},
400: {Description: "Validation error"},
},
}Nested structs
Nested structs are resolved recursively. Named structs become schema components (v1.3.0+); anonymous structs are inlined as object schemas.
type Company struct {
Name string `json:"name" description:"Company name" example:"Acme Corp"`
Address Address `json:"address" description:"Company address"`
}
type Address struct {
Street string `json:"street" example:"123 Main St"`
City string `json:"city" example:"Springfield"`
State string `json:"state" example:"IL"`
Zip string `json:"zip" example:"62701"`
}Since v1.3.0, Company and Address are both emitted under components.schemas, and the address property is {"$ref": "#/components/schemas/Address", "description": "Company address"}. Each struct is described once, however many endpoints use it; see Schema Components below. With InlineSchemas: true (and before v1.3.0), Address is instead expanded in place as a nested type: object property.
Schema Components (v1.3.0+)
Every named struct used in a request or response body, directly or nested through a field, slice element or map value, is emitted once under components.schemas and referenced with $ref:
type Address struct {
Street string `json:"street" example:"123 Main St"`
City string `json:"city" example:"Springfield"`
}
type Category struct {
ID int64 `json:"id"`
Name string `json:"name"`
Children []Category `json:"children"` // recursive
}
type User struct {
ID int64 `json:"id" swagger:"required"`
Name string `json:"name" swagger:"required"`
Address Address `json:"address" description:"Home address"`
Billing *Address `json:"billing" description:"Postal address"` // nullable
Interests []Category `json:"interests"`
Meta struct { // anonymous: stays inline
Source string `json:"source"`
} `json:"meta"`
}
docs.Add(openswag.Endpoint{
Method: "POST",
Path: "/users",
RequestBody: &openswag.RequestBody{Schema: User{}, Required: true},
Responses: openswag.Responses{
201: {Description: "Created", Schema: User{}},
},
})"requestBody": {
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/User" } }
},
"required": true
},
"components": {
"schemas": {
"User": {
"type": "object",
"properties": {
"id": { "type": "integer", "format": "int64" },
"name": { "type": "string" },
"address": { "$ref": "#/components/schemas/Address", "description": "Home address" },
"billing": {
"description": "Postal address",
"oneOf": [{ "$ref": "#/components/schemas/Address" }, { "type": "null" }]
},
"interests": { "type": "array", "items": { "$ref": "#/components/schemas/Category" } },
"meta": { "type": "object", "properties": { "source": { "type": "string" } } }
},
"required": ["id", "name"]
},
"Address": {
"type": "object",
"properties": {
"street": { "type": "string", "example": "123 Main St" },
"city": { "type": "string", "example": "Springfield" }
}
},
"Category": {
"type": "object",
"properties": {
"id": { "type": "integer", "format": "int64" },
"name": { "type": "string" },
"children": { "type": "array", "items": { "$ref": "#/components/schemas/Category" } }
}
}
}
}The response 201 uses the same {"$ref": "#/components/schemas/User"}.
What stays inline
- Anonymous structs (
struct{ ... }fields or values) are expanded in place. - Embedded structs are flattened into their parent as before (see Embedded Structs). The embedded type does not become a component, but named-struct fields inside it do.
- Parameters (
QueryParams,PathParamsand explicitParameters) are always inline. time.Timeis astringwithformat: date-time, not a component.
Recursive structs
A struct that refers back to itself (directly or through a slice, map or pointer) gets a real self-reference: Category.children.items is {"$ref": "#/components/schemas/Category"}, so code generators produce a properly recursive type. The {type: object, description: "Recursive reference to ..."} placeholder only appears with InlineSchemas: true.
Component names
- The name is the Go type name:
User,Address. - If two different types share a name, the first one registered keeps the bare name and the next gets its Go package name as a prefix:
dto.Userandmodel.UserbecomeUserandmodelUser. If that is taken too, a numeric suffix is added (modelUser2). Registration order is endpoint order; within an endpoint, the request body first, then responses by ascending status code, then struct fields in declaration order. - Generic type names are sanitised to the component key alphabet
[A-Za-z0-9_.-], with package paths reduced to the package name:Page[github.com/x/dto.User]becomesPage_dto.User.
Because a prefixed name depends on registration order, give same-named types in different packages distinct Go names if you need stable component keys.
Nullable references and field tags
A pointer to a named struct (Billing *Address) cannot use the ["object", "null"] type array, so the reference is wrapped:
"billing": { "oneOf": [{ "$ref": "#/components/schemas/Address" }, { "type": "null" }] }A field description is kept next to the reference: {"$ref": "...", "description": "Home address"} for a plain field, or on the outer object for a nullable one ({"description": "Postal address", "oneOf": [...]}). Every other tag on a struct-typed field (example, enum, format, validate bounds) is dropped, because it would constrain the shared component from the outside. Put those tags on the struct's own fields instead.
Effect on code generators
Generators now produce named types instead of anonymous objects. With openapi-typescript the body above is components['schemas']['User']; orval generates named User, Address and Category interfaces. See TypeScript Types.
Inlining everything (InlineSchemas)
Set Config.InlineSchemas to restore the pre-v1.3.0 output: no components.schemas, every struct expanded at each use, and recursive back-edges cut off with {type: object, description: "Recursive reference to ..."}.
docs := openswag.New(openswag.Config{
Info: openswag.Info{Title: "My API", Version: "1.0.0"},
InlineSchemas: true, // default false
})