OPEN SWAG GO
Features

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.

basic_schema.go
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:

generated_schema.yaml
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: 30

Supported 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 name
  • json:"-" — excludes the field from the schema
  • json:"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
}
ValueEffect
requiredAdds 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"`
}
Rulestringnumberslice / array
requiredAdds to requiredAdds to requiredAdds to required
min=N / gte=NminLength: Nminimum: NminItems: N
max=N / lte=NmaxLength: Nmaximum: NmaxItems: N
gt=NminLength: N+1minimum: N (approximation)minItems: N+1
lt=NmaxLength: N-1maximum: N (approximation)maxItems: N-1
len=NminLength and maxLength set to Nminimum and maximum set to NminItems and maxItems set to N
oneof=a b cenum: [a, b, c]enum with typed values—
emailformat: email——
uuid, uuid3, uuid4, uuid5format: uuid——
url, uriformat: uri——
ipv4, ipv6, hostnameformat: ipv4 / ipv6 / hostname——
dive,<rules>——<rules> apply to items

Notes:

  • gt and lt on numbers are emitted as the inclusive minimum / maximum; exclusiveMinimum and exclusiveMaximum are never written. For string lengths and item counts, which are integers, the exclusive bound is exact (N+1 / N-1).
  • oneof values are separated by spaces; wrap a value containing spaces in single quotes (oneof='dark blue' red). Values are converted to the field type, so an int field gets integer enum values. If the field is a pointer (nullable), null is appended to the enum.
  • Format rules only set format when it is not already set by a format tag.
  • Only a bare required marks a field required. Conditional rules such as required_if, required_with and required_without do not (v1.3.0+).
  • Rules containing | (alternatives), rules with parameters that do not parse as numbers, and unknown rules such as unique are 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 TypeOpenAPI TypeOpenAPI Format
stringstring—
int, int8, int16integer—
int32integerint32
int64integerint64
float32numberfloat
float64numberdouble
boolboolean—
[]Tarrayitems: T
structobject (named structs in bodies: $ref, v1.3.0+)—
map[string]TobjectadditionalProperties: T
time.Timestringdate-time
time.Durationstringduration (v1.1.0+)
[]bytestringbyte (v1.1.0+)
*T[T, "null"] (v1.3.0+)same as T
uuid.UUIDstringuuid
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.

embedded_structs.go
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+):

embedded_rules.go
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.

map_types.go
type Config struct {
	Labels   map[string]string `json:"labels"   description:"Key-value labels"`
	Metadata map[string]any    `json:"metadata" description:"Arbitrary metadata"`
}
generated_map_schema.yaml
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"}.

any_fields.go
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.

omitempty_required.go
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:

AnnotationStruct TagEffect
minItemsvalidate:"min=N" on slicesMinimum array length
maxItemsvalidate:"max=N" on slicesMaximum array length
annotations.go
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 TypeGenerated FormatDescription
time.DurationdurationISO 8601 duration
[]bytebyteBase64-encoded binary data
uuid.UUIDuuidUUID string

Practical Examples

Tagged struct with validation

tagged_struct.go
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:

schema_endpoint.go
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.

nested_structs.go
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:

components.go
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{}},
	},
})
openapi.json (excerpt)
"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, PathParams and explicit Parameters) are always inline.
  • time.Time is a string with format: 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.User and model.User become User and modelUser. 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] becomes Page_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
})