Pull to refresh

Go Code Generation from OpenAPI spec

Reading time4 min
Views12K

OpenAPI specification


One of the nicest features of Go is the power of code generation. go generate command serves as a Swish knife allowing you to generate enums, mocks and stubs. In this article, we will employ this feature to generate a Go code from OpenAPI specification. OpenAPI specification is a modern industrial standard for REST API. This standard has fantastic tooling support and allows you to conveniently render and validate the spec. We are going to befriend the power of Go code generation with the elegance and clarity of the OpenAPI specification. In this way, you don't have to manually update the Go boilerplate code after every change in the spec. You also ensure that your docs and your code are a single entity, as your code is being begotten from the docs.


Let's start dead-simple: we have a service that accepts order requests. Let's declare endpoint order/10045234 that accepts PUT requests, where 10045234 is an ID of a particular order. We expect to receive an order as a JSON payload in the following format.


    {"item":  "Tea Table Green", "price":  106}

How can describe this endpoint in the OpenAPI spec?


The skeleton of the spec looks the following:


    openapi: 3.0.3
    info: 
        title: Go and OpenAPI Are Friends Forever
        description: Let's do it
        version: 1.0.0
    paths:
    components:

First, we need to describe the response body — order and put this description under section components. In OpenAPI the order object looks the following:


components: 
  schemas: 
    Order:
      type: object
      properties: 
        item:
          type: string
        price:
          type: integer

It's a bit more verbose, but it is also very expressive. We can easily add validations rules on top of it. For example, we can enlist all possible items:


item:
  type: string
  enum:
    - Tea Table Green
    - Tea Table Red

OpenAPI specification is very rich and lets one easily specify that price must be a positive integer or that id must be in UUID format. We also can specify which fields are mandatory.


Now, let's specify our endpoint under section paths.


paths:
"/order/{id}":
    put:
      summary: Create an order
      parameters:
        - in: path
          description: Order ID
          name: id
          schema: 
            type: string
      requestBody:
        required: true
        content: 
          application/json:
            schema: 
              $ref: "#/components/schemas/Order"

The above part of the specification states that we expect a PUT request on the endpoint "/order/id" with the Order object as payload. Finally, let's add an expected response to the endpoint.


responses: 
    "201":
      description: The order was successfully created.

Code Generation


At this point we have the complete specification:


openapi: 3.0.3
info:
  title: Title
  description: Title
  version: 1.0.0
components:
  schemas:
    Order:
      type: object
      properties:
        item:
          type: string
          enum:
            - Tea Table Green
            - Tea Table Red
        id:
          type: string
        price:
          type: integer
paths:
  "/order/{id}":
    put:
      summary: Create an order
      parameters:
        - in: path
          description: Order ID
          name: id
          schema: 
            format: string
      requestBody:
        required: true
        content: 
          application/json:
            schema: 
              $ref: "#/components/schemas/Order"
      responses: 
        "201":
          description: The order was successfully created.

Now, the fun part: let the machine do its job and generate the code. To do so we will employ an awesome Go lib oapi-codegen. Download it with go get and add the following command to your Makefile:


gen:
    oapi-codegen -package spec spec.yaml > gen.go

Play Time!


Now, let's give it a try and see what we got from the generation. For the client side, we have NewClientWithResponses function that returns a ready-to-use client object. The generator also creates order object as PutOrderIdJsonRequestBody and enum values for item properties. You can notice that price and item expects pointers, as those fields are optional in our OpenAPI spec.


const server = "localhost:8080"

func main() {
   client, err := spec.NewClientWithResponses(server)
   if err != nil {
      log.Fatal(err)
   }

   item := spec.OrderItemTeaTableGreen
   price := 14
   resp, err := client.PutOrderIdWithResponse(context.Background(), "234578", spec.PutOrderIdJSONRequestBody{
      Item: &item, Price: &price,
   })
   if err != nil {
      log.Fatal(err)
   }

   fmt.Println(resp.StatusCode())
}

In less than 15 lines of hand-written code, we got a full-fledged client of our API! The OpenAPI standard has a tremendous palette of tooling and the code be generated for a variety of languages.


And for the server side, we got interface ServerInterface with a single method PutOrderId to implement. Let's declare a type server that conforms to this interface. By default, the code is generated for router echo, which can be downloaded as go get github.com/labstack/echo.


type server struct {}

func (s server) PutOrderId(c echo.Context, id string) error {
   var req spec.PutOrderIdJSONRequestBody
   if err := c.Bind(&req); err != nil {
      log.Fatal(err)
   }
   log.Printf("id: %v, req: %v", id, req)
   return nil
}

Now all we need to do is to register this handler:


func main() {
   e := echo.New()
   spec.RegisterHandlers(e, server{})

   e.Logger.Fatal(e.Start(address))
}

At this stage, we have a fully working client and server with a minimum amount of hand-written code. There is a ton of cool features we can add on top of that, for example, echo offers a middleware that will automatically validate the request payload and return detailed validation error. Overall, the usage of OpenAPI specification in combination with oapi-codegen generator and echo framework can radically increase the speed of web development by removing the necessity for hand-written boiler-plate code.


The specification and code can be found here.

Tags:
Hubs:
Total votes 3: ↑3 and ↓0+3
Comments0

Articles