github.com/libtnb/validator/contrib/openapi generates OpenAPI 3.1 documents
from Go request and response types. validate rules supply constraints;
documentation tags and typed options supply the API's descriptions and examples.
The module has no dependency on a web framework or application.
type CreateProduct struct {
Name string `json:"name" validate:"required && max:100" openapi:"description=Product name,example=Notebook"`
SKU string `json:"sku" validate:"required" openapi:"description=Stock keeping unit,example=NOTE-001"`
}
type Product struct {
ID int64 `json:"id" openapi:"description=Product ID,example=42,readOnly"`
Name string `json:"name" openapi:"description=Product name,example=Notebook"`
}
g := openapi.MustNew("Catalog API", "1.0.0",
openapi.WithServer("https://api.example.com", "Production"),
openapi.WithSecurityScheme("apiKey", openapi.APIKeyScheme("X-API-Key", openapi.HeaderLocation)),
openapi.WithDefaultSecurity(openapi.SecurityRequirement{"apiKey"}),
)
err := g.Add[CreateProduct](http.MethodPost, "/products",
openapi.WithOperationID("createProduct"),
openapi.WithSummary("Create product"),
openapi.WithDescription("Create a product in the catalog."),
openapi.WithTags("Products"),
openapi.WithRequestExample(CreateProduct{Name: "Notebook", SKU: "NOTE-001"}),
openapi.WithResponse[Product](http.StatusCreated,
openapi.ResponseDescription("Product created"),
openapi.ResponseExample(Product{ID: 42, Name: "Notebook"}),
),
)
// Handle err before rendering.
spec, err := g.JSON()New returns configuration errors; MustNew panics on them. Add is
transactional: a failed operation does not reserve an operation ID, allocate
component names, or change existing paths. JSON sorts map keys for reproducible
output. Document returns a deep copy that can be inspected or changed safely.
A generator is not safe for concurrent use.
The generator reads only the openapi documentation tag on JSON properties
and inferred uri / query parameters, including embedded structs, nested objects
and array elements. Other libraries' tags such as default:"5s" are ignored.
Properties inside openapi are comma-separated:
| Property | Meaning |
|---|---|
description |
Field or parameter description; may contain CommonMark. |
example |
A schema example, also attached to inferred parameters. |
default |
A documented default; it does not assign a value at runtime. |
deprecated |
Boolean flag; use it only when the field is deprecated. |
readOnly |
Boolean flag indicating a response-only property. |
writeOnly |
Boolean flag indicating a request-only property. |
Name string `json:"name" openapi:"description=Product name,example=Notebook"`
OldID string `json:"oldId" openapi:"description='Compatibility identifier, use id instead',deprecated"`
Tags []string `json:"tags" openapi:"description=Labels,example=[\"new\",\"sale\"]"`
Enabled bool `json:"enabled" openapi:"example=false,default=false"`Quote a value containing commas with single or double quotes. Quoted values support
escaped quotes, backslash, newline, carriage return, and tab; Go struct tag escaping
is applied first. JSON arrays and objects can be written directly without quoting
the entire value. deprecated, readOnly, and writeOnly without = mean true;
an explicit boolean value is also accepted. Unknown or duplicate properties are errors.
For a string schema, example=P-1 is string text and default='' is an empty
string. For other types the value must be JSON of the effective schema type,
including WithSchema overrides. Malformed JSON, primitive type mismatches,
invalid boolean flags, and simultaneous readOnly / writeOnly produce contextual
type-definition errors, just as malformed validate tags do. They are not
ErrInvalidOption. This is not full JSON Schema validation of example constraints.
Documentation tags do not change runtime validation. Field descriptions belong to the particular property or parameter, so two fields referencing the same component can have different descriptions.
WithOperationID(id)sets an explicit, nonblank ID, unique within the document.WithSummary,WithDescription,WithTags, andDeprecated()describe the operation.WithRequestDescription,RequestRequired(),RequestOptional(), andWithRequestExampledescribe the generated JSON body. They return an error for an operation without a body.WithParameter(Parameter{...})adds a path, query, header, or cookie parameter. If the same(in, name)was inferred, the explicit parameter replaces it completely, including its schema and required flag. Duplicate explicit parameters are rejected; header names are compared without case. A path parameter must be required and have a matching{name}placeholder.
openapi.WithParameter(openapi.Parameter{
Name: "X-Request-Time", In: "header", Required: true,
Description: "Request timestamp in Unix seconds",
Schema: &openapi.Schema{Type: "integer"},
Example: jsontext.Value("1700000000"),
})Authorization is described through security schemes; Accept and
Content-Type belong to media types. They cannot be added as header parameters.
The generator continues to infer JSON request bodies for POST, PUT, and PATCH;
adding metadata does not introduce a body for GET or DELETE.
Construct schemes with APIKeyScheme(name, location) or
HTTPScheme(scheme, bearerFormat), then register them with WithSecurityScheme.
API key locations are HeaderLocation, QueryLocation, and CookieLocation.
API-specific signing instructions belong in the scheme's Description field.
The package does not implement signing or authentication.
scheme := openapi.HTTPScheme("bearer", "JWT")
scheme.Description = "Send the access token issued by the identity service."
openapi.WithSecurityScheme("bearer", scheme)WithDefaultSecurity sets document-wide requirements. WithSecurity overrides
them for one operation:
// Both credentials required (AND).
openapi.WithSecurity(openapi.SecurityRequirement{"apiKey", "bearer"})
// Either credential accepted (OR).
openapi.WithSecurity(
openapi.SecurityRequirement{"apiKey"},
openapi.SecurityRequirement{"bearer"},
)
// Public operation: emit security: [], overriding document defaults.
openapi.WithoutSecurity()Omitting WithSecurity inherits the document setting. WithSecurity and
WithDefaultSecurity require a first requirement argument. Empty requirements,
duplicate scheme names within a requirement, and undefined scheme references are
errors. An empty computed list cannot silently become a public declaration;
that requires WithoutSecurity() explicitly. These scope-free requirements are
serialized as standard OpenAPI objects with empty scope arrays. OAuth scopes are
not exposed by this API. Default requirements may appear before their scheme
definitions in New's option list.
WithResponse[T] and WithDefaultResponse[T] accept ResponseDescription,
ResponseExample, and ResponseHeader options. NoBody means no response content,
not a JSON null body, and cannot have a response example.
openapi.WithResponse[any](http.StatusOK, openapi.ResponseExample(nil))
openapi.WithResponse[Problem](http.StatusTooManyRequests,
openapi.ResponseHeader("Retry-After", openapi.Header{
Description: "Seconds before retrying",
Schema: &openapi.Schema{Type: "integer"},
Example: jsontext.Value("1"),
}),
)Request and response example options serialize and snapshot their arguments.
nil, false, 0, "", [], and {} remain distinct values. In the document
model, examples, defaults and constants use jsontext.Value: a nil slice omits
the member, while jsontext.Value("null") emits an explicit JSON null. Response
headers use case-insensitive names; Content-Type comes from response content.
Use WithSchema[T] when a type's JSON representation differs from what reflection
can infer, such as a decimal that implements custom marshaling:
openapi.WithSchema[Money](&openapi.Schema{Type: "number"})Use WithSchemaTransform[T] for named Go types to supplement an inferred
schema without duplicating its fields. Built-in types such as string and
anonymous types are rejected; WithSchema remains available for scalar wire
formats. Schema supports AllOf, AnyOf, OneOf, and Not, alongside the
existing constraints and the new JSON-valued metadata.
openapi.WithSchemaTransform[Contact](func(schema *openapi.Schema) error {
schema.AnyOf = []*openapi.Schema{
{Required: []string{"email"}},
{Required: []string{"phone"}},
}
return nil
})Transforms run when a type schema is inferred, before per-use field rules and
metadata are attached. A struct's child properties already have their own rules and
documentation. Ordinary named struct components are inferred once; top-level
request bodies are inferred per operation. Inline named types and custom
marshalers are transformed on every occurrence, so a transform on a named scalar
affects every use of that named type, not built-in values with the same underlying
type. For a top-level request the schema contains only body fields; path and query
parameters remain separate. A whole-type WithSchema override has
precedence over a transform. Transforms return errors when an assumed field or
contract is missing, and a failed transform rolls back the entire Add.
Transforms describe constraints; they do not enforce them in the application.
Pointer layers are ignored when registering either option. Overrides and transform
results are copied, including numeric bounds and composition branches. Generic
component names use OpenAPI's permitted characters, including types instantiated
with any; name collisions still receive stable numeric suffixes.
From this directory:
go test ./...
go test -race ./...
go vet ./...To test against an unpublished root validator change, create a temporary Go workspace containing the repository root and this submodule, as the repository's contrib CI job does. No application-specific schema or fixture is required.