Skip to content

Latest commit

 

History

History
229 lines (189 loc) · 10.1 KB

README.md

File metadata and controls

229 lines (189 loc) · 10.1 KB

OpenAPI generation

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.

Field documentation

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.

Operation and request options

  • WithOperationID(id) sets an explicit, nonblank ID, unique within the document.
  • WithSummary, WithDescription, WithTags, and Deprecated() describe the operation.
  • WithRequestDescription, RequestRequired(), RequestOptional(), and WithRequestExample describe 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.

Authentication

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.

Responses and JSON values

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.

Custom wire types and schema transforms

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.

Tests

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.