Note: phiên bản Tiếng Việt của bài này ở link dưới.
https://duongnt.com/openapi-generator-mustache-customize-vie

The clients generated by OpenAPI Generator can work out of the box, but there might be cases where the default code is not optimized. Today, we’ll add our own customization to the output of OpenAPI Generator using Mustache template. This article assumes some familiarity with how OpenAPI Generator works and with the sample code in my previous article, I suggest everyone check it out first.
Again, you can download all the sample code from the link below.
https://github.com/duongntbk/OpenAPIGeneratorDemo
An actual problem in production
This is the simplified version of a problem I had in one of my projects. In my previous article, we generated clients for the Maritimum and Pecunia API. The generated clients can handle all HTTP errors and wrap them in an exception type called ApiException. However, each client has their own version of ApiException. Maritimum has this version under the MaritimumClient.Client namespace; and Pecunia has this version under the PecuniaClient.Client namespace.
If we look at the source code, we can see that they are exactly the same, except for the namespace. But as far as the framework is concerned, these are two completely different classes. This is a problem when we want to use both clients at the same time. If we want to handle exceptions for both APIs, we have to write something like this.
try
{
var portTask = portApi.PortsShowAsync(portUuid);
var accountTask = accountApi.AccountsShowAsync(accountUuid);
await Task.WhenAll(portTask, accountTask);
}
catch (MaritimumClient.Client.ApiException ex)
{
if (ex.ErrorCode == 404)
{
// Do something
}
// Do something else
}
catch (PecuniaClient.Client.ApiException ex)
{
if (ex.ErrorCode == 404)
{
// Do something
}
// Do something else
}
We have to repeat all those catch blocks every time we want to handle API exceptions. And if we have multiple APIs, things can get ugly quickly. It would be nice if all clients throw the same exception.
Mustache template in OpenAPI Generator
Introducing Mustache template
Mustache template can be considered as the blueprint that OpenAPI Generator uses to generate all clients code. There are template files for all classes of all languages that OpenAPI Generator supports. You can find the full list here. Because we want to customize our C# .Net Core client, we will focus on the templates in csharp-netcore folder.
As an example, let’s look at the HttpMethod template.
{{>partial_header}}
namespace {{packageName}}.Client
{
/// <summary>
/// Http methods supported by swagger
/// </summary>
public enum HttpMethod
{
// A list of HTTP method
}
}
And this is the generated code in MaritimumClient.
/*
* Maritimum
*
* a sample API to return ships and ports information.
*
* The version of the OpenAPI document: v1
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
namespace MaritimumClient.Client
{
/// <summary>
/// Http methods supported by swagger
/// </summary>
public enum HttpMethod
{
// The same list of HTTP Method
}
}
As we can see, all the C# code is copied verbatim to the generated class, while the part inside {{}} are replaced with user provided values. For example, {{packageName}} becomes MaritimumClient.
Create our own Mustache template
Since Mustache template is the blueprint for OpenAPI Generator, we can change the way it generates the clients by customizing the templates. Let’s try this with the template of ApiException. First, we need to download the default template from here. Then we will add a property to ApiException.
{{>visibility}} class ApiException : Exception
{
/// <summary>
/// Property to demo Mustache template
/// </summary>
public string TestProperty { get; set; } = "Test property for Mustache template";
/// <summary>
/// Gets or sets the error code (HTTP status code)
/// </summary>
/// <value>The error code (HTTP status code).</value>
public int ErrorCode { get; set; }
//... more code
}
After that, we save this custom template into a folder called CustomTemplate and re-generate MaritimumClient by running the following command.
openapi-generator-cli generate -i Maritimum/doc/openapi.yaml -g csharp-netcore -t <path to CustomTemplate folder> -o ../MaritimumClient --package-name MaritimumClient
As we can see, the custom template folder is passed to the t argument. This command will generate the following code.
public class ApiException : Exception
{
/// <summary>
/// Property to demo Mustache template
/// </summary>
public string TestProperty { get; set; } = "Test property for Mustache template";
/// <summary>
/// Gets or sets the error code (HTTP status code)
/// </summary>
/// <value>The error code (HTTP status code).</value>
public int ErrorCode { get; set; }
//... more code
}
The TestProperty property is included in ApiException as we expected. Note that we only have to overwrite the templates we need, not all templates. For templates we don’t provide, OpenAPI Generator will use the default version. Aim with this knowledge, we will try to wrap ApiException of all clients into a common exception class.
Define a common ApiException class
We will define a common exception class in a separate package and let all clients inherit it. That common exception looks very similar to a generated ApiException. You can find it in the following link.
Perhaps you have noticed that our ApiException lacks the Headers property. This is because Headers uses a generated type called Multimap. We don’t want the common exception to depend on any client, that’s why we omit it for now. In a later section, we will find out how to add Headers back to our common exception.
Let ApiException inherit our common exception
The first step is adding the CommonClient package as a dependency of our clients. We do that by modifying the netcore_project template. You can find the customized template here. This is the interesting part.
<ItemGroup>
<ProjectReference Include="..\CommonClient\CommonClient.csproj" />
</ItemGroup>
Notice that CommonClient is added as a project reference. This means the CommonClient folder must be in the same folder with our client projects. Obviously, in a real-life project, CommonClient will be published to a package repository. In that case, we need to modify the code above to use package reference.
<ItemGroup>
<PackageReference Include="XXX.CommonClient" Version="x.y.z" />
// other packages
</ItemGroup>
Then we can modify the template of ApiException, this is the result. Below are the changes we made.
- Added
CommonClient.Exceptions.ApiExceptionas the base class of client’sApiException.{{>visibility}} class ApiException : CommonClient.Exceptions.ApiException - Removed
ErrorCodeandErrorContentbecause those properties are already defined inside the base class. - Modified the constructors to call the constructors in base class. As mentioned earlier,
Headersdoes not exist in the base class. Because of this, the constructor which receivesHeaderslooks like this.public ApiException(int errorCode, string message, object errorContent = null, Multimap<string, string> headers = null) : base(errorCode, message, errorContent) { this.Headers = headers; }
Again, we re-generate MaritimumClient and PecuniaClient by running the commands below.
openapi-generator-cli generate -i Maritimum/doc/openapi.yaml -g csharp-netcore -t CustomTemplate -o ../MaritimumClient --package-name MaritimumClient
openapi-generator-cli generate -i Pecunia/doc/openapi.yaml -g csharp-netcore -t CustomTemplate -o ../PecuniaClient --package-name PecuniaClient
One exception to rule them all
Now, we can simplify the code in the opening section to this.
try
{
var portTask = portApi.PortsShowAsync(portUuid);
var accountTask = accountApi.AccountsShowAsync(accountUuid);
await Task.WhenAll(portTask, accountTask);
}
catch (CommonClient.Exeptions.ApiException ex)
{
if (ex.ErrorCode == 404)
{
// Do something
}
// Do something else
}
No matter how many APIs we call, as long as they all use the templates above, we can use one catch block to handle all exceptions. Moreover, if we ever need to handle exceptions from one API in a special way, we can always catch just that exception.
try
{
// code to call multiple APIs
}
catch (SpecialApi.Client.ApiException ex)
{
// Do special things
}
catch (CommonClient.Exeptions.ApiException ex)
{
// Do some normal things
}
Where to go from here?
The solution above works well enough, but there are still things we can improve.
- We need to omit
HeadersfromCommonClient.Exceptions.ApiException, it would be nice if we can include that property. - It’s debatable if wrapping all client exceptions in a common exception class is a clean solution. Perhaps client code should use
CommonClient.Exceptions.ApiExceptiondirectly?
We can archive all the goals above with customized templates. The actual solution is left as an exercise for the reader, but below are some hints.
- Define our own
Multimapclass. This is an easy step, you can copy a generated class intoCommonClientand change the namespace. - Let the clients use our own
Multimap. We need to modify the template ofApiException/ApiResponse/ClientUtils/RequestOptions/ApiClient/ClientUtilsto referenceCommonClient.Multimap.{{>partial_header}} using CommonClient; // other using statements - Add the
Headersproperty toCommonClient.Extensions.ApiExceptionand change the base class toSystem.Exception. - Let the clients use
CommonClient.Extensions.ApiExceptiondirectly. We do this by modifying the template ofXXXApi/ApiClient/Configurationto referenceCommonClient.Extensions.ApiException.{{>partial_header}} using CommonClient.Exceptions; // other using statements - Put all customized templates into the
CustomTemplatefolder. - Tell OpenAPI Generator to skip generating
Multimap.csandApiException.cs.- Create a file called
.openapi-generator-ignorein the root of MaritimumClient output folder. - Add these lines into
.openapi-generator-ignore.src/MaritimumClient/Client/ApiException.cs src/MaritimumClient/Client/Multimap.cs - Repeat the two steps above with PecuniaClient.
- Create a file called
- Re-generate all clients.
Conclusion
By using just a few simple template files, we have united all clients under one common exception. And although this article only focuses on C# .NET Core, the general principles also apply to all languages that OpenAPI Generator supports. With clever uses of Mustache template, we can customize every aspect of generated clients.