Note: phiên bản Tiếng Việt của bài này ở link dưới.

https://duongnt.com/openapi-generator-vie

openapi-generator

Writing client code to access an API is a time-consuming task. Not only do we have to select a suitable HTTP verb and connect to the correct endpoint, we also have to pass all parameters in the correct format and parse the returned values. Not to mention all the boiler code to handle connection and server errors. Or we can automate all those tasks with the OpenAPI Generator tool.

In this article, we will use OpenAPI Generator to automatically create API clients to access two APIs using only their OpenAPI Specification. All the sample uses C# .NET Core.

You can download all sample code from the repository below. It has all the sample APIs, auto-generated client libraries, and a demo program.

https://github.com/duongntbk/OpenAPIGeneratorDemo

Install OpenAPI Generator

Installing OpenAPI Generator is pretty straight forward. Personally, I used npm to install the CLI version on my Windows machine.

npm install @openapitools/openapi-generator-cli -g

However, you can also use Homebrew, Docker, Bash Script, or install it directly from Maven. Please see this link for more information.

We also need to install the newest Java Development Kit. I grabbed it from Oracle site.

After that, we can verify that OpenAPI Generator is installed by running this command.

openapi-generator-cli version

In my case, I installed version 5.3.0.

Did set selected version to 5.3.0
5.3.0

Generate clients

OpenAPI Generator can create API clients, server stubs, documentation and configuration for an API based on its OpenAPI Specification file. Today, we will generate the clients for two sample APIs.

  • Maritimum: this API returns dummy data about ships and ports. Here is its OpenAPI spec.
  • Pecunia: this API returns dummy data about people and their bank accounts. Here is its OpenAPI spec.

The command to generate client for Maritimum is.

openapi-generator-cli generate -i Maritimum/doc/openapi.yaml -g csharp-netcore -o <output folder> --package-name MaritimumClient

The meaning of each argument is below.

  • i: The path to openapi.yaml file of Maritimum.
  • g: The language to generate the client. We pass csharp-netcore here to generate client for C# .NET Core. OpenAPI Generator also supports many other languages. You can find the full list here.
  • package-name: the namespace for our client.

Inside the output folder, we can find the document in Markdown format, the unit test code, and the client code. We will focus on the client code folder.

  • Api folder: these are the classes we use to make calls to the API.
  • Client folder: these are the helper classes to make HTTP requests and process the responses.
  • Model folder: these are the mapping model classes.

Note that we have two Api accessor classes, ShipApi to call endpoints with tag value ship and PortApi to call endpoints with tag value port. If our endpoints do not have tag then all endpoints will be grouped into one class called DefaultApi. Personally, I think separating endpoints by tag helps keep the size of API classes down, which helps readability.

Likewise, below is the command to generate client for Pecunia.

openapi-generator-cli generate -i Pecunia/doc/openapi.yaml -g csharp-netcore -o <output folder> --package-name PecuniaClient

Use the generated clients to call API endpoints

Create API accessor object

As mentioned earlier, we use the XXXApi classes to call API endpoints. To retrieve ships information from Maritimum, we need to initialize a ShipApi object. The fastest way is to use this constructor.

var shipApi = new ShipApi("https://localhost:5003"); // https://localhost:5003 is the base path of Maritimum

If we want to fine-tune the API class, we can create a Configuration object and pass it to this constructor.

var configuration = new Configuration
{
    BasePath = "https://localhost:5003",
    AccessToken = "<access token>",
    UserAgent = "<custom user-agent",
    Timeout = 500_000, // set timeout to 500,000 milliseconds
    // ...
};
var shipApi = new ShipApi(configuration);

We can further control how HTTP requests are made by using this constructor. In this case, we need to implement the HttpClient classes ourselves.

Call endpoints

The first endpoint we try to call is ships.index. This endpoint does not take any parameter and returns all ships. The corresponding method in ShipApi is ShipsIndexAsync (we also have a ShipsIndex method, but we only focus on the asynchronous version for now).

var allShips = await shipApi.ShipsIndexAsync(); // allShips is a List<Ship>

That was easy, but ships.index is the simplest endpoint. Next, we will try ships.report, a more difficult one. This endpoint requires a home_port_uuid parameter, and we can choose to pass the maximum and minimum tonnage argument as well. Moreover, we need to use the HTTP-Post verb here. Let’s see how the generated client calls this endpoint.

var homePortUuid = new Guid("28153b3b-f3ac-45d1-820d-fec983a9aa56");
var maxTonnage = 35_000; // Only retrieve ships with tonnage above 35,000
var shipReportRequest = new ShipReportRequest(homePortUuid, maxTonnage);
var someShips = await shipApi.ShipsReportAsync(shipReportRequest); // someShips is List<Ship>

It’s not much harder than calling ships.index. The ShipsReportAsync method will automatically create the request body based on our argument and send that request body to the correct endpoint using POST. Then it parses the response into a list of Ship objects.

Maybe you’ve also noticed that we did not provide minTonnage, which is not a required parameter of ships.report. This is possible because ShipReportRequest.MinTonnage and ShipReportRequest.MaxTonnage are created as Nullable<int> based on the attribute nullable: true in the OpenAPI spec. Generally speaking, nullable parameters are mapped into optional arguments or nullable properties.

About the model classes

As we can see from the previous section, all components in the OpenAPI spec are mapped to corresponding model classes in the generated client. For example, the Ship component is mapped to the Ship class. But a model class is more than just a list of public properties. Printing a list of model objects is very simple.

foreach (var ship in someShips)
{
    Console.WriteLine(ship.ToString());
}

The Ship class overides the ToString method to provide a string presentation of our object. The code above will print this to the console.

class Ship {
  Uuid: e85382d2-552c-4745-b7fe-114c2d87e4c3
  Name: HQ-04
  HomePortUuid: 28153b3b-f3ac-45d1-820d-fec983a9aa56
  Tonnage: 40000
}

class Ship {
  Uuid: f336a003-fb63-48b3-8180-80c9f5541fc0
  Name: Seiraimaru
  HomePortUuid: f0d4d067-c806-4b1d-a8e6-2080504be61b
  Tonnage: 100000
}

Moreover, it also overrides the Equals method to compare ships based on their properties instead of comparing the references.

var uuid = Guid.NewGuid();
var homePortUuid = Guid.NewGuid();
var name = "dummyName";
var ship1 = new Ship(uuid, name, homePortUuid);
var ship2 = new Ship(uuid, name, homePortUuid);

Console.WriteLine(ship1.Equals(ship2)); // Will print "true"

Of course, we can still compare the references if needed.

Console.WriteLine(object.ReferenceEquals(ship1, ship2)); // Will print "false"

Exception handling

All exceptions occurring in API classes are wrapped in a ApiException. This custom exception class stores the error code, request body as json object, error message and HTTP headers. Let’s call the Maritimum API with a non-exists UUID.

try
{
    await ShipApi.ShipsShowAsync(Guid.NewGuid());
}
catch (ApiException ex)
{
    Console.WriteLine($"Error code: {exception.ErrorCode}");
    Console.WriteLine($"Error content: {exception.ErrorContent}");
    Console.WriteLine($"Error message: {exception.Message}");
}

This will print something like this.

Error code: 404
Error content: "Cannot find Ship with Uuid: <a random UUID>"
Error message: Error calling ShipsShow: "Cannot find Ship with Uuid: <a random UUID>"

Conclusion

Sometimes, being lazy is a good thing. And the OpenAPI Generator tool allows us to be "lazy" while still getting the work done. It’s not hard to imagine a workflow where API repositories are scanned periodically, and any change in OpenAPI spec will trigger a background process to re-generate all impacted API clients. Those clients can then be automatically pushed to their own repositories. This makes sure that our API clients are always up to date.

A software developer from Vietnam and is currently living in Japan.

One Thought on “Generate API clients with OpenAPI Generator”

Leave a Reply