NSwag vs Swashbuckle for Swagger, Typescript client API generation, and fighting undefined/nulls in DTO

I have already expressed my love with Swagger :) Over time, however, I met Swagger’s sister — NSwag — and fell in love with her even more :)

Long story short, NSwag doesn’t have an IFormFile issues I was solving in Swagger out of the box. But the reason I moved is actually a bit different. We wanted to use OpenAPI definitions for autogenerating clients for our API. Writing something like this:

export interface IFirmwareInfoDto {
    exists: boolean;
    versionString: string;
    releaseDate: string;
    deviceGeneration: DeviceGeneration;
}

async getAvailableFiles(options?: AxiosRequestConfig): Promise<IFirmwareInfoDto[]> {
    const response = await axios.get<IFirmwareInfoDto[]>(
        `/api/firmwareDownload/all`,
        {...defaultOpts, ...options},
    );
    return response.data;
},

by hand for every API action is not only tedious, but also error prone. Thank goodness, there are tools for automatic generation of those based on OpenAPI definitions. We used OpenAPI Generator initially (since Swashbuckle doesn’t have anything built-in), and it was good, but, as usual, devil was in the details. And the devil here was C# enum handling.


Actually, OpenAPI Generator had no issues with handling enum. It just doesn’t handle them at all :) There were just two options, both of which were affecting API itself: we could either express enum elements as string or as number. That was suboptimal. So, having a C# DTO and Action like this:

public class UserDto
{
	public string Name { get; set; }
	public DateTime BirthDate { get; set; }
	public Sex Sex { get; set; }
}

[HttpGet]
public UserDto Get(int id)
{
	return new UserDto()
	{
		Name = "Artur",
		BirthDate = new DateTime(1985, 5, 12),
		Sex = Sex.Male,
	};
}

We got the following Dto generated in Typescript

export interface UserDto {
    name?: string | null;
    birthDate?: Date;
    sex?: Sex;
}

export enum Sex {
    NUMBER_0 = 0,
    NUMBER_1 = 1
} 

We could get Sex enum generated like this:

export enum Sex {
    Male = 'Male',
    Female = 'Female'
} 

But only if we configure our API to behave the same way (i.e. you would receive the following JSON as a result of an http call

{
  'name': 'Artur',
  'birthDate': '2009-02-15T00:00:00Z',
  'sex': 'Male'
}

It’s a debatable topic, whether to use strings or ints for enums in the API, but in some cases (like Flags enums, where several values could be combined) ints are unavoidable.

So, we tried NSwag and thankfully, here’s what we have with it:

export enum Sex {
    Male = 0,
    Female = 1,
}

The key difference, is that NSwag has it’s own client generator, and it takes into account some of extensions that NSwag adds when generating API definition (i.e. string representation of enum values are stored in description fields).

Of course, NSwag also generates nice little (well, not so little :)) typescript client as well:

export class UserClient {
    constructor(baseUrl?: string, instance?: AxiosInstance) {
      // ...
    }

    get(id: number): Promise<UserDto> {
      // ...
    }
}

…and we have lived happily ever after with NSwag, unless we discovered another glitch. It’s called undefined. If we take a closer look at UserDto we could notice, that all properties are undefinable (this little ? sign next to the property name):

export interface IUserDto {
    name?: string | undefined;
    birthDate?: Date;
    sex?: Sex;
}

It’s not only inconvenient (because in typescript you would always have to check, if the value is really defined or not), but it’s also just wrong, because with aforementioned c# definition both birthDate and sex will always be defined.

Luckily, this could be fixed rather easy. We have introduced a special RequireValueTypesSchemaProcessor, that you could integrate into NSwag with oneliner:

options.SchemaProcessors.Add(new RequireValueTypesSchemaProcessor());

and DTOs will be automatically fixed for you:

export interface IUserDto {
    name: string;
    birthDate: Date;
    sex: Sex;
}

Go take a look at the sources if you want more details! You will also receive a bonus — the way to easily get back to generating undefinable for some DTOs where you particularly want that (e.g. for HTTP PATCH requests) :)

As usual, you could get complete C# project along with all generated client examples from github. Check out clients folder for already generated clients, or run yarn nswag-client or yarn swashbuckle-nswag-client to regenerate them.