Warning
OpenAPI support for JSON:API is currently experimental. The API and the structure of the OpenAPI document may change in future versions.
OpenAPI clients
After enabling OpenAPI, you can generate a typed JSON:API client for your API in various programming languages.
Note
If you prefer a generic JSON:API client instead of a typed one, choose from the existing client libraries.
The following code generators are supported, though you may try others as well:
- NSwag (v14.1 or higher): Produces clients for C# (requires
Newtonsoft.Json
) and TypeScript - Kiota: Produces clients for C#, Go, Java, PHP, Python, Ruby, Swift and TypeScript
For C# clients, we provide an additional package that provides workarounds for bugs in NSwag and enables using partial POST/PATCH requests.
To add it to your project, run the following command:
dotnet add package JsonApiDotNetCore.OpenApi.Client.NSwag --prerelease
Getting started
To generate your C# client, follow the steps below.
Visual Studio
The easiest way to get started is by using the built-in capabilities of Visual Studio. The following steps describe how to generate and use a JSON:API client in C#, combined with our NuGet package.
In Solution Explorer, right-click your client project, select Add > Service Reference and choose OpenAPI.
On the next page, specify the OpenAPI URL to your JSON:API server, for example:
http://localhost:14140/swagger/v1/swagger.json
. SpecifyExampleApiClient
as the class name, optionally provide a namespace and click Finish. Visual Studio now downloads your swagger.json and updates your project file. This adds a pre-build step that generates the client code.Tip
To later re-download swagger.json and regenerate the client code, right-click Dependencies > Manage Connected Services and click the Refresh icon.
Run package update now, which fixes incompatibilities and bugs from older versions.
Add our client package to your project:
dotnet add package JsonApiDotNetCore.OpenApi.Client.NSwag --prerelease
Add code that calls one of your JSON:API endpoints.
using var httpClient = new HttpClient(); var apiClient = new ExampleApiClient(httpClient); var getResponse = await apiClient.GetPersonCollectionAsync(new Dictionary<string, string?> { ["filter"] = "has(assignedTodoItems)", ["sort"] = "-lastName", ["page[size]"] = "5" }); foreach (var person in getResponse.Data) { Console.WriteLine($"Found person {person.Id}: {person.Attributes!.DisplayName}"); }
Extend the demo code to send a partial PATCH request with the help of our package:
var updatePersonRequest = new UpdatePersonRequestDocument { Data = new DataInUpdatePersonRequest { Id = "1", // Using TrackChangesFor to send "firstName: null" instead of omitting it. Attributes = new TrackChangesFor<AttributesInUpdatePersonRequest>(_apiClient) { Initializer = { FirstName = null, LastName = "Doe" } }.Initializer } }; await ApiResponse.TranslateAsync(async () => await _apiClient.PatchPersonAsync(updatePersonRequest.Data.Id, updatePersonRequest)); // The sent request looks like this: // { // "data": { // "type": "people", // "id": "1", // "attributes": { // "firstName": null, // "lastName": "Doe" // } // } // }
Tip
The example project contains an enhanced version
that uses IHttpClientFactory
for scalability and
resiliency and logs the HTTP requests and responses.
Additionally, the example shows how to write the swagger.json file to disk when building the server, which is imported from the client project.
This keeps the server and client automatically in sync, which is handy when both are in the same solution.
Other IDEs
The following section shows what to add to your client project file directly:
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.ApiDescription.Client" Version="8.0.*" PrivateAssets="all" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.*" />
<PackageReference Include="NSwag.ApiDescription.Client" Version="14.1.*" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<OpenApiReference Include="OpenAPIs\swagger.json">
<SourceUri>http://localhost:14140/swagger/v1/swagger.json</SourceUri>
<ClassName>ExampleApiClient</ClassName>
<OutputPath>%(ClassName).cs</OutputPath>
</OpenApiReference>
</ItemGroup>
From here, continue from step 3 in the list of steps for Visual Studio.
Configuration
Various switches enable you to tweak the client generation to your needs. See the section below for an overview.
The OpenApiReference
element can be customized using various NSwag-specific MSBuild properties.
See the source code for their meaning.
The JsonApiDotNetCore.OpenApi.Client.NSwag
package sets various of these for optimal JSON:API support.
Note
Earlier versions of NSwag required the use of <Options>
to specify command-line switches directly.
This is no longer recommended and may conflict with the new MSBuild properties.
For example, the following section puts the generated code in a namespace, makes the client class internal and generates an interface (handy when writing tests):
<OpenApiReference Include="swagger.json">
<Namespace>ExampleProject.GeneratedCode</Namespace>
<NSwagClientClassAccessModifier>internal</NSwagClientClassAccessModifier>
<NSwagGenerateClientInterfaces>true</NSwagGenerateClientInterfaces>
</OpenApiReference>
Headers and caching
The use of HTTP headers varies per client generator. To use ETags for caching, see the notes below.
To gain access to HTTP response headers, add the following in a PropertyGroup
or directly in the OpenApiReference
:
<NSwagWrapResponses>true</NSwagWrapResponses>
This enables the following code, which is explained below:
var getResponse = await ApiResponse.TranslateAsync(() => apiClient.GetPersonCollectionAsync());
string eTag = getResponse.Headers["ETag"].Single();
Console.WriteLine($"Retrieved {getResponse.Result?.Data.Count ?? 0} people.");
// wait some time...
getResponse = await ApiResponse.TranslateAsync(() => apiClient.GetPersonCollectionAsync(if_None_Match: eTag));
if (getResponse is { StatusCode: (int)HttpStatusCode.NotModified, Result: null })
{
Console.WriteLine("The HTTP response hasn't changed, so no response body was returned.");
}
The response of the first API call contains both data and an ETag header, which is a fingerprint of the response. That ETag gets passed to the second API call. This enables the server to detect if something changed, which optimizes network usage: no data is sent back, unless is has changed. If you only want to ask whether data has changed without fetching it, use a HEAD request instead.
Atomic operations
Atomic operations are fully supported. The example project demonstrates how to use them. It uses local IDs to:
- Create a new tag
- Create a new person
- Update the person to clear an attribute (using
TrackChangesFor
) - Create a new todo-item, tagged with the new tag, and owned by the new person
- Assign the todo-item to the created person
Known limitations
Limitation | Workaround | Links |
---|---|---|
Partial POST/PATCH sends incorrect request | Use TrackChangesFor from JsonApiDotNetCore.OpenApi.Client.NSwag package |
|
Exception thrown on successful HTTP status | Use TranslateAsync from JsonApiDotNetCore.OpenApi.Client.NSwag package |
https://github.com/RicoSuter/NSwag/issues/2499 |
No Accept header sent when only error responses define Content-Type |
JsonApiDotNetCore.OpenApi.Swashbuckle package contains workaround |
|
Schema type not always inferred with allOf |
JsonApiDotNetCore.OpenApi.Swashbuckle package contains workaround |
|
Generated code for JSON:API extensions does not compile | JsonApiDotNetCore.OpenApi.Swashbuckle package contains workaround |
|
A project can't contain both JSON:API clients and regular OpenAPI clients | Use separate projects |