Skip to content
Matthew Adams By Matthew Adams Co-Founder · 2 min read
OpenAPI Code Generation with Corvus: End-to-End

At endjin, we maintain Corvus.JsonSchema, and in the previous two posts we generated a typed HTTP client and server stubs from the same OpenAPI spec. This time we wire them together. A generated client calls a generated server over real HTTP, and we look at authentication with generated scope constants.

Why both sides from one spec?

The whole point of a contract-first approach is that the client and server agree. If you generate only the client, you can still drift on the server side. If you generate only the server, clients can send malformed requests that pass your hand-written validation but violate the spec.

When both sides are generated from the same specification, the schema contract is enforced at both boundaries. If the client builds a request that satisfies its generated validation, the server will accept it. If either side drifts from the spec, regeneration breaks the build. That's the feedback loop we want.

Setting up the round-trip

Generate both sides with separate namespaces:

corvusjson openapi-client petstore.json \
    --rootNamespace Petstore.Client \
    --outputPath ./Generated/Client

corvusjson openapi-server petstore.json \
    --rootNamespace Petstore.Server \
    --outputPath ./Generated/Server

Start the server with ASP.NET Core minimal APIs:

using Corvus.Text.Json.OpenApi;
using Corvus.Text.Json.OpenApi.HttpTransport;
using Petstore.Server;
using Petstore.Client;

WebApplicationBuilder builder = WebApplication.CreateBuilder();
WebApplication app = builder.Build();

PetsHandler handler = new();
app.MapApiEndpoints(handler);

await app.StartAsync();
string serverUrl = app.Urls.First();

Connect the client via HttpClientTransport:

using HttpClient httpClient = new() { BaseAddress = new Uri(serverUrl) };
await using HttpClientTransport transport = new(httpClient);
await using ApiPetsClient client = new(transport);

Both sides validate against the same JSON Schema. If the client builds a valid request, the server accepts it. If either side drifts from the spec, validation fails at the boundary.

Full round-trip

Create a resource through the client, receive it from the server:

await using CreatePetResponse createResponse = await client.CreatePetAsync(
    session_token: "admin-token"u8,
    body: new NewPet.Source((ref NewPet.Builder b) =>
    {
        b.Create(
            name: "Luna"u8,
            status: "available"u8,
            tags: new NewPet.JsonStringArray.Source(
                (ref NewPet.JsonStringArray.Builder ab) =>
                {
                    ab.AddItem("friendly"u8);
                    ab.AddItem("vaccinated"u8);
                }));
    }));

createResponse.MatchResult(
    matchCreated: pet =>
    {
        Console.WriteLine($"Created: [{pet.Id}] {pet.Name}");
        return 0;
    },
    matchDefault: error =>
    {
        Console.WriteLine($"Error: {error.Message}");
        return 0;
    });

The generated server handler receives pre-validated parameters and returns typed results:

public ValueTask<CreatePetResult> HandleCreatePetAsync(
    CreatePetParams parameters,
    JsonWorkspace workspace,
    CancellationToken cancellationToken = default)
{
    return new(CreatePetResult.Created(
        body: new Pet.Source((ref Pet.Builder pb) =>
        {
            pb.Create(
                id: 42,
                name: parameters.Body.Name,
                status: parameters.Body.Status);
        }),
        workspace: workspace));
}

What's next

So far we've focused on the request/response basics. In the [ref slug=openapi-code-generation-with-corvus-streaming text=next post], we'll look at streaming - SSE and NDJSON responses where the server pushes a sequence of typed items and the client consumes them as IAsyncEnumerable.

FAQs

Can I generate both the client and server from the same OpenAPI spec? Yes. Run corvusjson openapi-client and corvusjson openapi-server on the same spec with different namespaces. Both sides share the same model types and validation rules, guaranteeing contract compliance.
How does Corvus handle OAuth2 authentication? Corvus generates per-operation OAuth2 scope constants from the spec's security requirements. You use these with standard .NET DelegatingHandler middleware and Azure.Identity - no proprietary auth framework needed.
Does the generated code support streaming (SSE and NDJSON)? Yes. Server responses declared as text/event-stream or application/x-ndjson produce a writer callback on the server and IAsyncEnumerable on the client. Items are typed, validated, and framed automatically.

Matthew Adams

Co-Founder

Matthew Adams

Matthew was CTO of a venture-backed technology start-up in the UK & US for 10 years, and is now the co-founder of endjin, which provides technology strategy, experience and development services to its customers who are seeking to take advantage of Microsoft Azure and the Cloud.