Customization
Overview
Metadata allows you to control and customize code generation through simple
tags. Use the Meta function to add metadata to your design elements.
Basic Type Generation Control
By default, Goa only generates types that are used by service methods. If you define a type in your design but don’t reference it in any method parameters or results, Goa will skip generating it.
The "type:generate:force" metadata tag overrides this behavior. It takes
service names as arguments to specify which services should include the type in
their generated code. If no service names are provided, the type will be
generated for all services:
var MyType = Type("MyType", func() {
// Force type generation in service1 and service2, even if unused
Meta("type:generate:force", "service1", "service2")
Attribute("name", String)
})
var OtherType = Type("OtherType", func() {
// Force type generation in all services
Meta("type:generate:force")
Attribute("id", String)
})
Package Organization
You can control where types are generated using package metadata. By default, types are generated in their respective service packages, but you can generate them in a shared package. This is particularly useful when multiple services need to work with the same Go structs, such as when sharing business logic or data access code. By generating types in a shared package, you avoid having to convert between duplicate type definitions across services:
var CommonType = Type("CommonType", func() {
// Generate in shared types package
Meta("struct:pkg:path", "types")
Attribute("id", String)
Attribute("createdAt", String)
})
This creates a structure like:
project/
├── gen/
│ └── types/ # Shared types package
│ └── common_type.go # Generated from CommonType
Important Notes
- All related types must use the same package path - Types that reference each other must be in the same package - The `types` package is commonly used for shared types - Using a shared package eliminates the need to copy or convert between duplicate type definitions when services share codeField Customization
By default, Goa generates field names by converting attribute names to CamelCase. For example, an attribute named “user_id” would become “UserID” in the generated struct.
Goa also provides default type mappings from design types to Go types:
String→stringInt→intInt32→int32Int64→int64Float32→float32Float64→float64Boolean→boolBytes→[]byteAny→any
You can customize individual fields using several metadata tags:
struct:field:name: Override the generated field namestruct:field:type: Override the generated field typestruct:tag:*: Add custom struct tags
Here’s an example combining these:
var Message = Type("Message", func() {
Meta("struct:pkg:path", "types")
Attribute("id", String, func() {
// Override field name
Meta("struct:field:name", "ID")
// Add custom MessagePack tag
Meta("struct:tag:msgpack", "id,omitempty")
// Override type with custom type
Meta("struct:field:type", "bison.ObjectId", "github.com/globalsign/mgo/bson", "bison")
})
})
This generates the following Go struct:
type Message struct {
ID bison.ObjectId `msgpack:"id,omitempty"`
}
Important Limitations
When using `struct:field:type`: - The overridden type must support the same marshaling/unmarshaling as the original type - Goa generates encoding/decoding code based on the original type definition - Incompatible marshaling behavior will cause runtime errorsProtocol Buffer Customization
When working with Protocol Buffers, you can customize the generated protobuf code using several metadata keys:
Message Type Names
The struct:name:proto metadata allows you to override the generated protobuf
message name. By default, Goa uses the type name from your design:
var MyType = Type("MyType", func() {
// Changes the protobuf message name to "CustomProtoType"
Meta("struct:name:proto", "CustomProtoType")
Field(1, "name", String)
})
Field Types
The struct:field:proto metadata lets you override the generated protobuf field
type. This is particularly useful when working with well-known protobuf types or
types from other proto files. It accepts up to four arguments:
- The protobuf type name
- (Optional) The proto file import path
- (Optional) The Go type name
- (Optional) The Go package import path
var MyType = Type("MyType", func() {
// Simple type override
Field(1, "status", Int32, func() {
// Changes from default sint32 to int32
Meta("struct:field:proto", "int32")
})
// Using Google's well-known timestamp type
Field(2, "created_at", Timestamp, func() {
Meta("struct:field:proto",
"google.protobuf.Timestamp", // Proto type
"google/protobuf/timestamp.proto", // Proto import
"Timestamp", // Go type
"google.golang.org/protobuf/types/known/timestamppb") // Go import
})
})
This generates the following protobuf definition:
import "google/protobuf/timestamp.proto";
message MyType {
int32 status = 1;
google.protobuf.Timestamp created_at = 2;
}
Import Paths
The protoc:include metadata specifies import paths used when invoking the
protoc compiler. You can set it at either the API or service level:
var _ = API("calc", func() {
// Global import paths for all services
Meta("protoc:include",
"/usr/include",
"/usr/local/include")
})
var _ = Service("calculator", func() {
// Service-specific import paths
Meta("protoc:include",
"/usr/local/include/google/protobuf")
// ... service methods ...
})
When set on an API definition, the import paths apply to all services. When set on a service, the paths only apply to that specific service.
Important Notes
- The `struct:field:proto` metadata must provide all necessary import information when using external proto types - Import paths in `protoc:include` should point to directories containing .proto files - Service-level import paths are additional to API-level paths, not replacementsOpenAPI Specification Control
Basic OpenAPI Settings
Control the generation and formatting of OpenAPI specifications:
var _ = API("MyAPI", func() {
// Control OpenAPI generation
Meta("openapi:generate", "false")
// Format JSON output
Meta("openapi:json:prefix", " ")
Meta("openapi:json:indent", " ")
})
This affects how the OpenAPI JSON is formatted:
{
"openapi": "3.0.3",
"info": {
"title": "MyAPI",
"version": "1.0"
}
}
Operation and Type Customization
You can customize how operations and types appear in the OpenAPI spec using several metadata keys:
Operation IDs
The openapi:operationId metadata lets you customize how operation IDs are
generated. It supports special placeholders that get replaced with actual
values:
{service}- replaced with the service name{method}- replaced with the method name(#{routeIndex})- replaced with the route index (only when a method has multiple routes)
For example:
var _ = Service("UserService", func() {
Method("ListUsers", func() {
// Generates operationId: "users/list"
Meta("openapi:operationId", "users/list") // Static value
})
Method("CreateUser", func() {
// Generates operationId: "UserService.CreateUser"
Meta("openapi:operationId", "{service}.{method}")
})
Method("UpdateUser", func() {
// For multiple routes, generates:
// - "UserService_UpdateUser_1" (first route)
// - "UserService_UpdateUser_2" (second route)
Meta("openapi:operationId", "{service}_{method}_{#routeIndex}")
HTTP(func() {
PUT("/users/{id}") // First route
PATCH("/users/{id}") // Second route
})
})
})
Operation Summaries
By default, if no summary is provided via metadata, Goa generates a summary by:
- Using the method’s description if one is defined
- If no description exists, using the HTTP method and path (e.g., “GET /users/{id}”)
The openapi:summary metadata allows you to override this default behavior. The
summary appears at the top of each operation and should provide a brief
description of what the operation does.
You can use:
- A static string
- The special placeholder
{path}which gets replaced with the operation’s HTTP path
var _ = Service("UserService", func() {
Method("CreateUser", func() {
// Uses this description as the default summary
Description("Create a new user in the system")
HTTP(func() {
POST("/users")
})
})
Method("UpdateUser", func() {
// Overrides the default summary
Meta("openapi:summary", "Handle PUT request to {path}")
HTTP(func() {
PUT("/users/{id}")
})
})
Method("ListUsers", func() {
// No description or summary metadata
// Default summary will be: "GET /users"
HTTP(func() {
GET("/users")
})
})
})
This generates the following OpenAPI specification:
{
"paths": {
"/users": {
"post": {
"summary": "Create a new user in the system",
"operationId": "UserService.CreateUser",
// ... other operation details ...
},
"get": {
"summary": "GET /users",
"operationId": "UserService.ListUsers",
// ... other operation details ...
}
},
"/users/{id}": {
"put": {
"summary": "Handle PUT request to /users/{id}",
"operationId": "UserService.UpdateUser",
// ... other operation details ...
}
}
}
}
Best Practices
- Keep summaries concise but descriptive - Use consistent wording across related operations - Consider including key parameters or constraints in the summary - Use {path} when the HTTP path provides important contextType Names
The openapi:typename metadata allows you to override how a type appears in the
OpenAPI specification, without affecting the Go type name:
var User = Type("User", func() {
// In OpenAPI spec, this type will be named "CustomUser"
Meta("openapi:typename", "CustomUser")
Attribute("id", Int)
Attribute("name", String)
})
Example Generation
Goa allows you to specify examples for your types in the design. If no examples
are specified, Goa generates random examples by default. The openapi:example
metadata lets you disable this example generation behavior:
var User = Type("User", func() {
// Specify an example (this will be used in the OpenAPI spec)
Example(User{
ID: 123,
Name: "John Doe",
})
Attribute("id", Int)
Attribute("name", String)
})
var Account = Type("Account", func() {
// Disable example generation for this type
// No examples will appear in the OpenAPI spec
Meta("openapi:example", "false")
Attribute("id", Int)
Attribute("balance", Float64)
})
var _ = API("MyAPI", func() {
// Disable example generation for all types
Meta("openapi:example", "false")
})
Note
- By default, Goa generates random examples for types without explicit examples - Use the `Example()` DSL to specify custom examples - Use `Meta("openapi:example", "false")` to prevent any example generation - Setting `openapi:example` to false at the API level affects all typesTags and Extensions
You can add tags and custom extensions to your OpenAPI specification at both the service and method levels. Tags help group related operations, while extensions allow you to add custom metadata to your API specification.
Service-Level Tags
When applied at the service level, tags are available to all methods in that service:
var _ = Service("UserService", func() {
// Define tags for the entire service
HTTP(func() {
// Add a simple tag
Meta("openapi:tag:Users")
// Add a tag with description
Meta("openapi:tag:Backend:desc", "Backend API Operations")
// Add documentation URL to the tag
Meta("openapi:tag:Backend:url", "http://example.com/docs")
Meta("openapi:tag:Backend:url:desc", "API Documentation")
})
// All methods in this service will inherit these tags
Method("CreateUser", func() {
HTTP(func() {
POST("/users")
})
})
Method("ListUsers", func() {
HTTP(func() {
GET("/users")
})
})
})
Method-Level Tags
You can also apply tags to specific methods, either adding to or overriding service-level tags:
var _ = Service("UserService", func() {
Method("AdminOperation", func() {
HTTP(func() {
// Add additional tags just for this method
Meta("openapi:tag:Admin")
Meta("openapi:tag:Admin:desc", "Administrative Operations")
POST("/admin/users")
})
})
})
Custom Extensions
Extensions can be added at multiple levels to customize different parts of the OpenAPI specification:
var _ = API("MyAPI", func() {
// API-level extension
Meta("openapi:extension:x-api-version", `"1.0"`)
})
var _ = Service("UserService", func() {
// Service-level extension
HTTP(func() {
Meta("openapi:extension:x-service-class", `"premium"`)
})
Method("CreateUser", func() {
// Method-level extension
HTTP(func() {
Meta("openapi:extension:x-rate-limit", `{"rate": 100, "burst": 200}`)
POST("/users")
})
})
})
This generates the following OpenAPI specification:
{
"info": {
"x-api-version": "1.0"
},
"tags": [
{
"name": "Users",
"description": "User management operations"
},
{
"name": "Backend",
"description": "Backend API Operations",
"externalDocs": {
"description": "API Documentation",
"url": "http://example.com/docs"
}
},
{
"name": "Admin",
"description": "Administrative Operations"
}
],
"paths": {
"/users": {
"post": {
"tags": ["Users", "Backend"],
"x-service-class": "premium",
"x-rate-limit": {
"rate": 100,
"burst": 200
}
},
"get": {
"tags": ["Users", "Backend"]
}
},
"/admin/users": {
"post": {
"tags": ["Users", "Backend", "Admin"],
"x-service-class": "premium"
}
}
}
}
Important Notes
- Service-level tags apply to all methods in the service - Method-level tags are added to service-level tags - Extensions can be added at API, service, and method levels - Extension values must be valid JSON strings - Tags help organize and group related operations in API documentationTesting Custom Types
When working with custom types and field overrides, it’s important to test that your types behave correctly. Here’s how to effectively test custom type implementations using Clue’s mock package:
// Import Clue's mock package
import (
"github.com/goadesign/clue/mock"
)
// Example custom type with overridden field type
type Message struct {
ID bison.ObjectId `msgpack:"id,omitempty"`
}
// Mock implementation using Clue's mock package
type mockMessageStore struct {
*mock.Mock // Embed Clue's Mock type
}
// Store implements the mock using Clue's Next pattern
func (m *mockMessageStore) Store(ctx context.Context, msg *Message) error {
if f := m.Next("Store"); f != nil {
return f.(func(context.Context, *Message) error)(ctx, msg)
}
return errors.New("unexpected call to Store")
}
func TestMessageStore(t *testing.T) {
// Create mock store using Clue's mock package
store := &mockMessageStore{mock.New()}
tests := []struct {
name string
msg *Message
setup func(*mockMessageStore)
wantErr bool
}{
{
name: "successful store",
msg: &Message{
ID: bison.NewObjectId(),
},
setup: func(m *mockMessageStore) {
m.Set("Store", func(ctx context.Context, msg *Message) error {
return nil
})
},
wantErr: false,
},
{
name: "store error",
msg: &Message{
ID: bison.NewObjectId(),
},
setup: func(m *mockMessageStore) {
m.Set("Store", func(ctx context.Context, msg *Message) error {
return fmt.Errorf("storage error")
})
},
wantErr: true,
},
{
name: "invalid message",
msg: &Message{}, // Empty ID
setup: func(m *mockMessageStore) {
m.Set("Store", func(ctx context.Context, msg *Message) error {
if msg.ID.IsZero() {
return fmt.Errorf("invalid message ID")
}
return nil
})
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create fresh mock for each test
mock := &mockMessageStore{mock.New()}
if tt.setup != nil {
tt.setup(mock)
}
// Execute test
err := mock.Store(context.Background(), tt.msg)
// Verify error behavior
if (err != nil) != tt.wantErr {
t.Errorf("Store() error = %v, wantErr %v", err, tt.wantErr)
}
// Verify all expected calls were made
if mock.HasMore() {
t.Error("not all expected operations were performed")
}
})
}
}
This example demonstrates several key features of Clue’s mock package:
- Type-Safe Mocking: The mock implementation (
mockMessageStore) provides a type-safe interface by embedding Clue’sMocktype - Custom Type Handling: Test custom type validation and behavior with proper field types
- Sequence Control: Use
Addfor ordered operations when needed - Default Behaviors: Use
Setfor consistent responses across test cases - Comprehensive Verification: The
HasMoremethod ensures all expected operations were performed
The test cases demonstrate comprehensive coverage of key scenarios. They verify that successful storage operations complete as expected and that error conditions are properly handled when storage failures occur. The tests also validate that custom type fields meet required constraints, ensuring data integrity. Finally, they confirm proper cleanup by verifying that all mock expectations are met during test execution.