Goa takes a pragmatic approach to validation, balancing performance with robustness. The framework validates data at system boundaries while trusting internal operations:
This approach ensures your code always receives valid data while avoiding unnecessary validation overhead for internal operations.
The code generation algorithms in Goa carefully consider when to use pointers for struct fields. The goal is to minimize pointer usage while maintaining type safety and proper null handling.
When Goa generates code, it needs to make decisions about how to represent
fields in the generated structs. One of the key decisions is whether to use a
pointer (*) or a direct value (-) for primitive types (like string
, int
,
bool
, etc.).
Before diving into the rules, let’s clarify the key terms:
Payload/Result: These are the method arguments and return values in your service design
method (payload *CreateUserPayload)
)returns (UserResult)
)Request/Response Bodies: These are the HTTP or gRPC transport-level structures
For example, in a REST API:
// In your design:
var _ = Service("users", func() {
Method("create", func() {
Payload(func() {
Field(1, "name", String) // This is a payload field
Required("name")
})
Result(func() {
Field(1, "id", Int) // This is a result field
})
HTTP(func() {
POST("/users")
Response(StatusOK)
})
})
})
// Goa generates:
type CreatePayload struct {
Name string // Payload field
}
type CreateRequestBody struct {
Name *string // Request body field
}
type CreateResult struct {
ID int // Result field
}
type CreateResponseBody struct {
ID int // Response body field
}
The rules vary depending on:
Here’s a detailed breakdown:
Properties | Payload/Result | Request Body (Server) | Response Body (Server) | Request Body (Client) | Response Body (Client) |
---|---|---|---|---|---|
Required OR Default | Direct (-) | Pointer (*) | Direct (-) | Direct (-) | Pointer (*) |
Not Required, No Default | Pointer (*) | Pointer (*) | Pointer (*) | Pointer (*) | Pointer (*) |
Let’s break this down with examples:
Required or Default Value Fields:
name string
field in a payload will be generated as Name string
Optional Fields (Not Required, No Default):
age int
field will be generated as Age *int
Special Types:
[]string
or map[string]int
(not *[]string
or *map[string]int
)The reasoning behind these rules:
Example Scenario:
// For a design with these fields:
// - name: string (required)
// - age: int (optional)
// - hobbies: []string
// - metadata: map[string]string
// The generated struct in the service package would look like:
type Person struct {
Name string // required, direct value
Age *int // optional, pointer
Hobbies []string // array, no pointer
Metadata map[string]string // map, no pointer
}
Default values specified in the design are used in two key scenarios:
When marshaling data for output, default values play an important role in handling nil values. For array and map fields that are nil, the default values specified in the design are used to initialize them. However, this doesn’t apply to primitive fields since they cannot be nil - they always have their zero value (0 for numbers, "" for strings, etc).
When unmarshaling incoming data, default values are only applied to optional fields that are missing from the input. If a required field is missing, this will trigger a validation error instead of applying a default value. For gRPC specifically, there is special handling for default values during unmarshaling - see the gRPC Unmarshaling section for details.