OneOf: Union Attributes in Goa
Announcing support for union attributes in Goa
- March 18, 2022
- Raphael Simon
Goa v3.7.0: Union Attributes in Goa
Goa v3.7.0 adds the ability to define union attributes in the Goa DSL. Union attributes enumerate multiple potential attribute values for a single type attribute.
Union attributes are defined using the new OneOf
function. OneOf
may be used
wherever Attribute
or Field
can be used, for example:
var Dog = Type("Dog", func() {
Description("Dogs are cool")
Field(1, "Name")
Field(2, "Breed")
Required("Name", "Breed")
})
var Cat = Type("Cat", func() {
Description("Cats are cool too")
Field(1, "Name")
Field(2, "Age", Int)
Required("Name", "Age")
})
var PetOwner = Type("PetOwner", func() {
Description("A pet owner")
OneOf("pet", func() {
Description("The owner's pet")
Field(1, "dog", Dog)
Field(2, "cat", Cat)
})
})
Union attributes are generated as Go interfaces that implement a private method. Each potential attribute type implements the private method thereby guaranteeing that only attributes with these types may be used to assign a value to the corresponding field.
The example above causes Goa to generate the following code for the service level types:
// A pet owner
type PetOwner struct {
// The owner's pet
Pet interface {
petVal()
}
}
// Cats are cool too
type Cat struct {
Name string
Age int
}
// Dogs are cool
type Dog struct {
Name string
Breed string
}
func (*Cat) petVal() {}
func (*Dog) petVal() {}
The type PetOwner
can be used as follows:
owner := gensvc.PetOwner{
Pet: &gensvc.Cat{
Name: "Garfield",
Age: 9,
},
}
// ...
owner.Pet = &gensvc.Dog{
Name: "Odie",
Breed: "Pitbull",
}
Code may also check for the current value using the standard Go switch
statement:
switch owner.Pet.(type) {
case *gensvc.Cat:
// ...
case *gensvc.Dog:
// ...
}
Transport Mapping
HTTP
Whenever an union attribute is encoded for HTTP Goa generates a Go struct with a
Type
string field that contains the name the current underlying type of the
attribute value and an interface{}
Value
field that contains the value:
// The owner's pet
Pet *struct {
// Union type name, one of:
// - "Dog"
// - "Cat"
Type *string `form:"Type" json:"Type" xml:"Type"`
// Union value, type must be one of service package types listed above
Value interface{} `form:"Value" json:"Value" xml:"Value"`
} `form:"pet,omitempty" json:"pet,omitempty" xml:"pet,omitempty"`
The code generated by Goa takes care of transforming the service level struct containing the interface value to and from the HTTP transport struct making it transparent to the service level code.
The generated OpenAPI specification for the service leverages JSON schema’s
AnyOf
property to describe the union attribute:
pet:
type: object
properties:
Type:
type: string
description: |-
Union type name, one of:
- "Dog"
- "Cat"
example: Cat
enum:
- Dog
- Cat
Value:
description: Union value, type must be one of service package types
listed above
example: Dog
anyOf:
- $ref: '#/components/schemas/Dog'
- $ref: '#/components/schemas/Cat'
description: The owner's pet
required:
- Type
- Value
gRPC
For services using gRPC Union types are mapped using the oneof
proto3
statement:
message Request {
oneof pet {
Dog dog = 1;
Cat cat = 2;
}
}
As with HTTP, Goa takes care of mapping the service level struct from and to the
struct generated by protoc
.