Metadata allows you to control and customize code generation through simple
tags. Use the Meta
function to add metadata to your design elements.
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)
})
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
types
package is commonly used for shared typesBy 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
→ string
Int
→ int
Int32
→ int32
Int64
→ int64
Float32
→ float32
Float64
→ float64
Boolean
→ bool
Bytes
→ []byte
Any
→ 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 tagsHere’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"`
}
When using struct:field:type
:
When working with Protocol Buffers, you can customize the generated protobuf code using several metadata keys:
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)
})
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:
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;
}
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.
struct:field:proto
metadata must provide all necessary import information when using external proto typesprotoc:include
should point to directories containing .proto filesControl 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"
}
}
You can customize how operations and types appear in the OpenAPI spec using several metadata keys:
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
})
})
})
By default, if no summary is provided via metadata, Goa generates a summary by:
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:
{path}
which gets replaced with the operation’s HTTP pathvar _ = 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 ...
}
}
}
}
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)
})
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")
})
Example()
DSL to specify custom examplesMeta("openapi:example", "false")
to prevent any example generationopenapi:example
to false at the API level affects all typesYou 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.
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")
})
})
})
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")
})
})
})
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"
}
}
}
}
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:
mockMessageStore
) provides a type-safe interface by embedding Clue’s Mock
typeAdd
for ordered operations when neededSet
for consistent responses across test casesHasMore
method ensures all expected operations were performedThe 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.