# OpenAPI Adapter - Hot Chocolate

> Expose REST endpoints from a Hot Chocolate GraphQL schema with the AddOpenApi() adapter: annotate operations with @http and get generated OpenAPI docs.

Canonical source: https://chillicream.com/docs/hotchocolate/adapters/openapi

The OpenAPI adapter exposes your Hot Chocolate GraphQL schema as REST endpoints with automatic OpenAPI documentation. You define GraphQL operations annotated with `@http` directives, and the adapter generates HTTP endpoints that accept REST-style requests, execute the underlying GraphQL operation, and return the result as JSON. The generated endpoints appear in your OpenAPI specification alongside any other ASP.NET Core endpoints.

This is useful when you have a GraphQL API and need to provide a REST interface for clients that do not support GraphQL, or when you want to offer both GraphQL and REST access to the same backend.

## Setup

Install the `HotChocolate.Adapters.OpenApi` package:

Bash

```
dotnet add package HotChocolate.Adapters.OpenApi
```

Register the adapter on your GraphQL server and map the endpoints:

C#

```
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddRouting()
    .AddOpenApi(options => options.AddGraphQLTransformer());

builder
    .AddGraphQL()
    .AddQueryType<Query>()
    .AddMutationType<Mutation>()
    .AddOpenApiDefinitionStorage(myStorage);

var app = builder.Build();

app.UseRouting();
app.UseEndpoints(endpoints =>
{
    endpoints.MapOpenApi();
    endpoints.MapOpenApiEndpoints();
    endpoints.MapGraphQL();
});

app.Run();
```

`AddOpenApiDefinitionStorage()` registers the adapter services and provides the endpoint definitions. `AddGraphQLTransformer()` adds a document transformer that injects the generated endpoints into your OpenAPI specification. `MapOpenApiEndpoints()` registers the dynamic HTTP endpoints at runtime.

## Endpoint Definitions

Each REST endpoint is defined by a GraphQL operation annotated with an `@http` directive. You provide these operations through an `IOpenApiDefinitionStorage` implementation.

A GET endpoint that fetches a user by ID:

GraphQL

```
"Fetches a user by their id"
query GetUserById($userId: ID!) @http(method: GET, route: "/users/{userId}") {
  userById(id: $userId) {
    id
    name
    email
  }
}
```

A POST endpoint that creates a user:

GraphQL

```
"Creates a user"
mutation CreateUser($user: UserInput! @body)
@http(method: POST, route: "/users") {
  createUser(user: $user) {
    id
    name
    email
  }
}
```

The `@http` directive specifies the HTTP method and route. Route parameters like `{userId}` map to GraphQL variables. The `@body` directive on a variable indicates that the HTTP request body maps to that variable.

## How It Works

The adapter translates between REST and GraphQL concepts:

| REST Concept                 | GraphQL Concept                     |
| ---------------------------- | ----------------------------------- |
| HTTP method (GET, POST, PUT) | Specified by @http(method: ...)     |
| Route path                   | @http(route: "/path/{param}")       |
| Route parameters             | GraphQL variables matched by name   |
| Query parameters             | Variables listed in queryParameters |
| Request body                 | Variable annotated with @body       |
| Response body                | Selected fields from the operation  |

When a client sends an HTTP request to a generated endpoint, the adapter extracts route parameters, query parameters, and the request body, maps them to GraphQL variables, executes the operation, and returns the root field's data as the response body.

## Route Parameters

Route parameters in curly braces map to GraphQL variables by name:

GraphQL

```
query GetUser($userId: ID!) @http(method: GET, route: "/users/{userId}") {
  userById(id: $userId) {
    id
    name
  }
}
```

A request to `GET /users/42` sets `$userId` to `"42"`.

You can also map route parameters to nested fields of a variable using the `key:$variable.path` syntax:

GraphQL

```
mutation UpdateUser($user: UserInput! @body)
@http(method: PUT, route: "/users/{userId:$user.id}") {
  updateUser(user: $user) {
    id
    name
  }
}
```

A PUT request to `/users/42` with a JSON body sets the `id` field of the `$user` variable to `"42"`, and the rest of the body fills in the remaining fields.

## Query Parameters

Use the `queryParameters` argument on the `@http` directive to expose GraphQL variables as URL query parameters:

GraphQL

```
query GetUserDetails($userId: ID!, $includeAddress: Boolean!)
@http(
  method: GET
  route: "/users/{userId}/details"
  queryParameters: ["includeAddress"]
) {
  userById(id: $userId) {
    id
    name
    address @include(if: $includeAddress) {
      street
    }
  }
}
```

A request to `GET /users/1/details?includeAddress=true` sets `$includeAddress` to `true`.

Query parameters support the same `key:$variable.path` mapping syntax as route parameters.

## Request Body

The `@body` directive on a variable maps the entire HTTP request body to that variable:

GraphQL

```
mutation CreateUser($user: UserInput! @body)
@http(method: POST, route: "/users") {
  createUser(user: $user) {
    id
    name
    email
  }
}
```

A POST request with a JSON body `{"id": "6", "name": "Alice", "email": "alice@example.com"}` sets `$user` to that object. The request must have a `Content-Type: application/json` header.

## Shared Fragments

You can define reusable GraphQL fragments as separate documents. The adapter resolves fragment references across documents:

GraphQL

```
# Document 1: endpoint definition
query GetUser($userId: ID!) @http(method: GET, route: "/users/{userId}") {
  userById(id: $userId) {
    ...UserFields
  }
}

# Document 2: shared fragment
fragment UserFields on User {
  id
  name
  email
  address {
    ...AddressFields
  }
}

# Document 3: another shared fragment
fragment AddressFields on Address {
  street
}
```

Each document is a separate entry in your `IOpenApiDefinitionStorage`. Fragment-only documents are treated as shared models.

### One model per document

A fragment-only document defines exactly one shared model. The **first** fragment in the document names the model, and that name is what other documents spread. Any further fragments in the same document are private helpers: fragments within that document can spread them, but no other document can reference them by name.

GraphQL

```
# A model named "User", plus a helper only this document can spread
fragment User on User {
  ...UserContact
  id
  name
}

fragment UserContact on User {
  email
  address {
    street
  }
}
```

To make a fragment referenceable from another document, give it a document of its own.

### Fragment name uniqueness

Fragment names must be unique across every document that contributes to a single endpoint, which is the endpoint's own document plus each model it spreads, transitively. When two of those documents define a fragment with the same name, the endpoint's composed document is invalid and the endpoint responds with HTTP 500\. Private helper fragments are not namespaced by their document, so this applies to them as well.

An endpoint that spreads a fragment no document defines is also invalid, and likewise responds with HTTP 500\. In both cases the endpoint is still routed, and it is omitted from the OpenAPI specification. Register a diagnostic event listener (see [Diagnostics](#diagnostics)) to see the reason.

## Storage

The `IOpenApiDefinitionStorage` interface provides endpoint and fragment definitions to the adapter:

C#

```
using HotChocolate.Adapters.OpenApi;
using HotChocolate.Adapters.OpenApi.Storage;
using HotChocolate.Language;

public class MyOpenApiStorage : IOpenApiDefinitionStorage
{
    public ValueTask<IEnumerable<IOpenApiDefinition>>
        GetDefinitionsAsync(
            CancellationToken cancellationToken = default)
    {
        var documents = new List<IOpenApiDefinition>();

        var getUserDoc = Utf8GraphQLParser.Parse(
            """
            query GetUser($userId: ID!)
              @http(method: GET, route: "/users/{userId}") {
              userById(id: $userId) {
                id
                name
              }
            }
            """);
        documents.Add(OpenApiDefinitionParser.Parse(getUserDoc));

        return ValueTask.FromResult<IEnumerable<IOpenApiDefinition>>(
            documents);
    }

    // IOpenApiDefinitionStorage also extends IObservable, enabling
    // hot-reload when definitions change.
    public IDisposable Subscribe(
        IObserver<OpenApiDefinitionStorageEventArgs> observer)
        => /* your subscription logic */;
}
```

Register it with your GraphQL server:

C#

```
var storage = new MyOpenApiStorage();

builder
    .AddGraphQL()
    .AddQueryType<Query>()
    .AddOpenApiDefinitionStorage(storage);
```

The storage implements `IObservable<OpenApiDefinitionStorageEventArgs>`. When you push `Updated` or `Removed` events through this observable, the adapter picks up changes at runtime, adding, updating, or removing HTTP endpoints without a restart. This hot-reload behavior extends to the OpenAPI specification.

## Diagnostics

A definition that the adapter cannot use is skipped rather than throwing at startup, so an endpoint can be routed and still fail every call with HTTP 500\. Derive from `OpenApiDiagnosticEventListener` to observe why:

C#

```
using HotChocolate.Adapters.OpenApi;

public class LoggingOpenApiDiagnosticEventListener(
    ILogger<LoggingOpenApiDiagnosticEventListener> logger)
    : OpenApiDiagnosticEventListener
{
    public override void ValidationErrors(
        IReadOnlyList<OpenApiDefinitionValidationError> errors)
    {
        foreach (var error in errors)
        {
            logger.LogError("{ValidationError}", error.Message);
        }
    }
}
```

Register it on the GraphQL server. The listener is activated from the schema services, so any application service it takes as a constructor argument, `ILogger<T>` included, must be made available with `AddApplicationService<T>()`:

C#

```
builder
    .AddGraphQL()
    .AddQueryType<Query>()
    .AddOpenApi()
    .AddApplicationService<ILogger<LoggingOpenApiDiagnosticEventListener>>()
    .AddDiagnosticEventListener<LoggingOpenApiDiagnosticEventListener>();
```

The listener receives an error for every definition the adapter cannot use: one that fails a validation rule, an endpoint whose composed document does not validate against the schema, an endpoint that could not be initialized, and a definition that could not be added to the OpenAPI document. Errors are reported whenever definitions are loaded or reloaded, so a hot-reload that breaks an endpoint reports it at that moment.

## OpenAPI Specification

The adapter integrates with ASP.NET Core's built-in OpenAPI support. After you register `AddOpenApi(options => options.AddGraphQLTransformer())`, the generated endpoints appear in the OpenAPI document at `/openapi/v1.json`.

Each endpoint definition's description becomes the OpenAPI operation summary. Route and query parameters become OpenAPI parameters with types inferred from the GraphQL schema. Request body schemas are generated from the GraphQL input types.

## Fusion Integration

The OpenAPI adapter works with Fusion gateway servers. Replace `AddGraphQL()` with `AddGraphQLGateway()` and the rest of the configuration remains the same:

C#

```
builder
    .AddGraphQLGateway()
    .AddInMemoryConfiguration(compositeSchema)
    .AddHttpClientConfiguration("Subgraph", subgraphUri)
    .AddOpenApiDefinitionStorage(myStorage);
```

The Fusion gateway composes schemas from multiple subgraphs. The OpenAPI adapter generates REST endpoints that execute operations against the composed schema, so a single REST endpoint can fetch data from multiple subgraphs transparently.

## Next Steps

- [MCP](https://chillicream.com/docs/hotchocolate/adapters/mcp) to expose your GraphQL schema as MCP tools for AI agents.
- [Errors](https://chillicream.com/docs/hotchocolate/resolvers/errors) to customize error responses in generated endpoints.
[Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/adapters/openapi.md)

Last updated on **August 12, 2026** by **Glen**
