Open API (Part 3): Generating an OpenAPI spec file from code
Overview
This is the 3rd article in a series of posts about working with Open API to create generated API clients and documentation websites.
In the 1st post we went over what we can do with Open API spec files, e.g. generate API clients and documentation websites.
In the 2nd post we went over how we can create the Open API spec file from an API contract-first approach, using a hand-crafted approach to create the definition of the API, and then having teams build against that.
In this 3rd post, we’ll go over how we can annotate our code to support generating the Open API spec file, which means developers won’t have to touch any YAML or JSON files to write API definitions.
What does using annotations to generate an Open API spec look like?
Code can be annotated in a variety of ways to support generating an Open API spec file. This is supported to various degrees in different programming languages.
Java and Kotlin
Probably one of the best supported languages for doing it this way is Java (and Kotlin). The Open API tools were originally written in (and for) Java. And while they were probably amongst the first set of tools available for devs to leverage for this purpose, Java and languages interoperable with Java aren’t the only ones to have these tools. Before going onto those, I’ll share the approach that can be used in Kotlin and Java projects.
Using the starter Spring web project generated from this website, you can add this dependency:
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0")
Assuming you want to make a simple todo list app, this could be the handler to retrieve all todos, accepting a query to filter if you want only completed or incomplete todos:
@RestController
@RequestMapping("/api/todos")
class TodoController {
private val todos = mutableListOf<Todo>(
Todo("Drink coffee", true),
Todo("Write blog post about handcrafting bespoke artisanal Open API specs", true),
Todo("Write blog post about creating Open API specs from code annotations", false),
)
@Operation(summary = "Gets all of your todos")
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "Success")
]
)
@GetMapping("", produces = [MediaType.APPLICATION_JSON_VALUE])
@ResponseStatus(HttpStatus.OK)
fun getTodos(
@PathVariable @RequestParam(name = "is_completed", required = false) filterState: Boolean?
): List<Todo> {
val response = todos
.filter { todo ->
if (filterState == null) return@filter true
filterState == todo.isCompleted
}
return response
}
}
And the Todo
class itself would be annotated as well:
@Schema(description = "Pending or completed item")
data class Todo(
@field:Schema(
description = "The task that needs to be completed",
example = "Wash car",
type = "string"
)
val text: String,
@field:Schema(
description = "Completed state of the todo",
type = "boolean"
)
val isCompleted: Boolean,
)
From these code snippets, you can see @Schema
, @field:Schema
, @Operation
, @ApiResponses
. The pattern here is generally there are annotations that map to the respective properties in the spec.
We can see that after it’s configured and we annotate the handler, we can get the information to show up in the Swagger UI tool.
There is of course other methods to support, e.g. post and put requests that accept a request body, and annotating those correctly.
TypeScript
In TypeScript, there’s a framework called Nest.js that is quite similar to Spring in that it’s also an object-oriented MVC framework. Nest can do something similar with the @nestjs/swagger library. Here’s an example controller for a stocks-related app:
import { Body, Controller, Get, Post } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';
import { Stock } from './stock.entity';
import { StockService } from './stock.service';
import { StocksByNameRequest } from './stocks-by-name.dto';
@ApiTags('stocks')
@Controller('stock')
export class StockController {
constructor(private stockService: StockService) {}
@ApiOperation({
summary: 'Get all available stock prices',
})
@ApiResponse({
status: 200,
description: 'List of stocks',
type: [Stock],
})
@Get()
async findAll(): Promise<Stock[]> {
return this.stockService.findAll();
}
@ApiOperation({
summary: 'Get stock prices by name',
})
@ApiResponse({
status: 200,
description: 'List of stocks',
type: [Stock],
})
@Post()
async findByNames(
@Body() stocksByNameRequest: StocksByNameRequest,
): Promise<Stock[]> {
const names = stocksByNameRequest.names;
return this.stockService.bulkFindByName(names);
}
}
// Requests
class StocksByNameRequest {
@ApiProperty({
description: 'List of stock tickers',
example: ['AAPL', 'TSLA'],
})
@IsNotEmpty()
names: string[];
}
You can see similar OpenAPI-related annotations, @ApiResponse
, @ApiOperation
, @ApiTags
, @ApiProperty
, and @IsNotEmpty
, the last of which comes from the class-validator library which is used for object validation for request objects.
This also carries through to the Stock
class:
import { ApiProperty } from '@nestjs/swagger';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity({
name: 'stocks',
})
export class Stock {
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({
description: 'Stock ticker',
example: 'AAPL',
})
@Column()
name: string;
@ApiProperty({
description: 'Price in US dollars',
example: 100,
})
@Column()
price: number;
}
Again we can see a similar pattern in that it maps properties in the spec.
If we look at the output JavaScript, we can see that Swagger code does end up getting bundled with the dist files.
Go lang
This has also been attempted in Go lang using Go’s structure tags. Struct tags are commonly used in Go for mapping fields of a struct to other objects like database fields or JSON fields. Here’s a simple example of how struct tags are commonly used for JSON marshalling and unmarshalling:
var person *struct {
Name string `json:"name"`
Address *struct {
Street string `json:"street"`
City string `json:"city"`
} `json:"address"`
}
There’s a project that aims to provide the same code generation abilities for Open API in Go that leverages these struct tags. The code looks like this:
type req struct {
ID string `path:"id" example:"XXX-XXXXX"`
Locale string `query:"locale" pattern:"^[a-z]{2}-[A-Z]{2}$"`
Title string `json:"title"`
Amount uint `json:"amount"`
Items []struct {
Count uint `json:"count"`
Name string `json:"name"`
} `json:"items"`
}
It also requires you to run lines of code in order to register objects into the Open API document. From the project’s documentation, we can see we need to execute quite a bit of code in order to generate the OpenAPI spec:
// Source: https://github.com/swaggest/openapi-go/tree/cdbf0a73418c416c59000778488f751a58f7cc54?tab=readme-ov-file#example
reflector := openapi3.Reflector{}
reflector.Spec = &openapi3.Spec{Openapi: "3.0.3"}
reflector.Spec.Info.
WithTitle("Things API").
WithVersion("1.2.3").
WithDescription("Put something here")
type req struct {
ID string `path:"id" example:"XXX-XXXXX"`
Locale string `query:"locale" pattern:"^[a-z]{2}-[A-Z]{2}$"`
Title string `json:"string"` // This looks like a typo and should be `json:"title"`
Amount uint `json:"amount"`
Items []struct {
Count uint `json:"count"`
Name string `json:"name"`
} `json:"items"`
}
type resp struct {
ID string `json:"id" example:"XXX-XXXXX"`
Amount uint `json:"amount"`
Items []struct {
Count uint `json:"count"`
Name string `json:"name"`
} `json:"items"`
UpdatedAt time.Time `json:"updated_at"`
}
putOp, err := reflector.NewOperationContext(http.MethodPut, "/things/{id}")
handleError(err)
putOp.AddReqStructure(new(req))
putOp.AddRespStructure(new(resp), func(cu *openapi.ContentUnit) { cu.HTTPStatus = http.StatusOK })
putOp.AddRespStructure(new([]resp), func(cu *openapi.ContentUnit) { cu.HTTPStatus = http.StatusConflict })
reflector.AddOperation(putOp)
getOp, err := reflector.NewOperationContext(http.MethodGet, "/things/{id}")
handleError(err)
getOp.AddReqStructure(new(req))
getOp.AddRespStructure(new(resp), func(cu *openapi.ContentUnit) { cu.HTTPStatus = http.StatusOK })
reflector.AddOperation(getOp)
schema, err := reflector.Spec.MarshalYAML()
if err != nil {
log.Fatal(err)
}
fmt.Println(string(schema))
This is quite a lot of code to write to support an OpenAPI spec but it supports the generation of the Open API spec YAML file.
Why (and why not) use annotations to generate the OpenAPI spec?
Let’s go over some of the reasons why you would or wouldn’t want to use this approach.
Why you may want to use this approach:
- The documentation is right near the code, so if you change the code, you may be more likely to update the documentation since it’s right there
Why you may not want to use this approach:
Same reason: the documentation is right near the code. This could, arguably, muck up the code, in that it adds more you need to skim past when maintaining or debugging this code. It can be argued that maybe verbose documentation doesn’t belong there.
You need to learn the abstraction layer of whatever library you’re using. This can be tedious and arguably not a valuable thing to learn and memorize. The initial learning curve can be higher than learning the spec outright since there are tools to help with code completion and validation of the schema file.
Depending on the language (e.g. Go lang), you may end up writing and shipping a significant amount of extra code related to documenting the Open API spec when using this approach. You may even end up writing more in code than you would have in YAML. Depending on when the code runs and if it’s a compile time operation or a runtime operation, we may create bugs or crashes in the application.
There may be other reasons to prefer or not prefer this approach, but these are the ones I thought of immediately. If you have reasons you like or dislike this approach, feel free to drop a comment.
Summary
This is the 3rd article in a series of articles about working with Open API. In this post we looked at 3 programming languages where we can generate an Open API spec and sometimes Swagger UI documentation website by annotating code or also by writing lines of code inline. Depending on the language, this can result in a significant amount of code that needs to be written.
In part 1 we went over how to generate API clients in TypeScript and API Reference documentation websites.
In part 2 we went over hand-coding Open API spec files ourselves.
If you have anything to add to the discussion, feel free to leave a comment!
Subscribe to my newsletter
Read articles from Tina Holly directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Tina Holly
Tina Holly
Hi, my name is Tina! I am a Full Stack Software Developer and Tech Lead from Canada. 🙋🏻♀️