OneOf: Union Attributes in Goa

Announcing support for union attributes in Goa

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.