Working with Data Types


An important aspect of the goa DSL resides around how types are defined and used. The Overview covers the basics of working with types and media types. This document takes a step back and explains the rationale for the DSL.

Data structures are described in the design using the Attribute function or one of its aliases (Member, Header or Param). This description exists in the absolute - that is it is not relative to a given language (e.g. Go) or technology. This makes it possible to generate many outputs ranging from Go code to documentation in the form of JSON schema and Swagger or binding to other languages (e.g. JavaScript clients). The design language includes a number of primitive types listed in the Overview but also makes it possible to describe arbitrary data structures recursively via the Type function.

Request Payload

One of the main application for types defined that way is to describe the request payload of a given action. The request payload describes the shape of the request body. goagen uses that description to generate the corresponding Go struct that the action method receives in its request context. Payloads may be defined inline as follows:

Action("create", func() {
	Routing(POST(""))
	Payload(func() {
		Member("name")
	})
	Response(Created, "/accounts/[0-9]+")
})

or can use a predefined type:

var CreatePayload = Type("CreatePayload", func() {
	Attribute("name")
})

Action("create", func() {
	Routing(POST(""))
	Payload(CreatePayload)
	Response(Created, "/accounts/[0-9]+")
})

The former notation ends up creating an anonymous type used internally by the generators. Note that the DSL is recursive, the example above does not specify a type for the name attribute thereby defaulting to String. But it could have used any other type including a data structure defined inline or via a predefined type:

Action("create", func() {
	Routing(POST(""))
	Payload(func() {
		Member("address", func() {
			Attribute("street")
			Attribute("number", Integer)
		})
	})
	Response(Created, "/accounts/[0-9]+")
})

or:

Action("create", func() {
	Routing(POST(""))
	Payload(func() {
		Member("name", Address) // where Address is a predefined type
	})
	Response(Created, "/accounts/[0-9]+")
})

That same flexibility exists wherever attributes are used.

Predefined types may be referred to using a variable as shown above or using their name, that is Payload(CreatePayload) could also be written as Payload("CreatePayload") since "CreatePayload" is the name given in the CreatePayload type definition. This makes it possible to define types that depend on each other and not have the Go compiler complain about the cycle.

Media Types

Another common use for types is for describing response media types. A response media type defines the shape of response bodies. Media types differ from types in that they also define views and links, see the Overview for details. Media types are defined using the MediaType function.

A basic media type definition may looks like this:

var MT = MediaType("application/vnd.app.mt", func() {
	Attributes(func() {
		Attribute("name")
	})
	View("default", func() {
		Attribute("name")
	})
})

The first argument of MediaType is the media type identifier as defined by RFC 6838. The DSL lists the attributes similarly to how attributes are defined in types, the views - here only the default view - and optionally links to other media types.

Such a media type may then be used to define the response of an action as follows:

Action("show", func() {
	Routing(GET("/:accountID"))
	Params(func() {
		Param("accountID", Integer, "Account ID")
	})
	Response(OK, func() {
		Media(MT)
	})
	Response(NotFound)
})

which is equivalent to:

Action("show", func() {
	Routing(GET("/:accountID"))
	Params(func() {
		Param("accountID", Integer, "Account ID")
	})
	Response(OK, MT)
	Response(NotFound)
})

or:

Action("show", func() {
	Routing(GET("/:accountID"))
	Params(func() {
		Param("accountID", Integer, "Account ID")
	})
	Response(OK, "application/vnd.app.mt")
	Response(NotFound)
})

Media types can be referred to using variables or the media type identifier similarly to how types can be referred to using variables or their names.

It is often the case that the same attributes are used to define an action request payload and a response media type. This is especially true with REST APIs where sending a request to create a resource often times returns a representation of it in the response. The goa design language helps with this common case by providing a Reference function that may be used in both Type and MediaType function calls alike. This function takes one argument which is either a variable holding a type or a media type or the name of a type or media type. Using the Reference function makes it possible to reuse the attributes of the type being referred to without having to redefine all its properties (type, description, example, validations etc.).

Given the following type definition:

var CreatePayload = Type("CreatePayload", func() {
	Attribute("name", String, "Name of thingy", func() {
		MinLength(5)
		MaxLength(256)
		Pattern("^[a-zA-Z]([a-zA-Z ]+)")
	})
})

A media type may take advantage of the name attribute definition like this:

var MT = MediaType("application/vnd.app.mt", func() {
	Reference(CreatePayload)
	Attributes(func() {
		Attribute("name")
	})
	View("default", func() {
		Attribute("name")
	})
})

The name attribute automatically inherits from the type, description and validations defined in the corresponding CreatePayload attribute. Note that the media type definition still needs to list the names of attributes being referred to, this makes it possible to pick and choose which attributes to “inherit”. Also the media type may override properties of the name attribute if needed (e.g. to change the type, description, validations etc.).

Media types can also reference themselves by using the media type identifier. This makes it possible to define recursive media types and not have the Go compiler complain about the cycles:

var MT = MediaType("application/vnd.app.mt", func() {
	Reference(CreatePayload)
	Attributes(func() {
		Attribute("name")
		Attribute("children", CollectionOf("application/vnd.app.mt"))
	})
	View("default", func() {
		Attribute("name")
	})
})

Mixing Types and Media Types

We’ve already seen how Reference makes it possible to reuse attribute definitions across types and media types. Media types are a specialized form of types. This means that they may be used in place of types wherever types can be used (anywhere an attribute is defined).

The best practice though consists of using media types solely for defining the response bodies as shown above and use types for everything else. This is because media types define additional properties such as an identifier, views and links that only apply to that use case. So define types and take advantage of the Reference keyword when the same attributes are shared between types and media types.