Customization

Learn how to customize and extend Goa’s code generation using metadata.

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

Field 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:

  • Stringstring
  • Intint
  • Int32int32
  • Int64int64
  • Float32float32
  • Float64float64
  • Booleanbool
  • Bytes[]byte
  • Anyany

You can customize individual fields using several metadata tags:

  • struct:field:name: Override the generated field name
  • struct:field:type: Override the generated field type
  • struct: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"`
}

Protocol 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:

  1. The protobuf type name
  2. (Optional) The proto file import path
  3. (Optional) The Go type name
  4. (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.

OpenAPI 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:

  1. Using the method’s description if one is defined
  2. 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 ...
      }
    }
  }
}

Type 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")
})

Tags 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"
      }
    }
  }
}

Testing 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:

  1. Type-Safe Mocking: The mock implementation (mockMessageStore) provides a type-safe interface by embedding Clue’s Mock type
  2. Custom Type Handling: Test custom type validation and behavior with proper field types
  3. Sequence Control: Use Add for ordered operations when needed
  4. Default Behaviors: Use Set for consistent responses across test cases
  5. Comprehensive Verification: The HasMore method 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.