> ## Documentation Index
> Fetch the complete documentation index at: https://geni.masiting.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Customizing the Document

> Change generated OpenAPI documents with transformers, custom rule mappings, exception responses, servers and operation IDs.

## Overview

Geni infers most of your document automatically. When you need to change the result (add a description, document a rate limit response, map a custom validation rule), register an extension through `Geni::configure()` or the `extensions` config key.

## `Geni::configure()`

Register extensions in a service provider's `boot()` method:

```php app/Providers/AppServiceProvider.php theme={null}
use Geni\Inference\Document\OpenApiDocument;
use Geni\Inference\Document\Operation;
use Geni\Laravel\DiscoveredRoute;
use Geni\Laravel\Geni;

public function boot(): void
{
    Geni::configure()
        ->withDocumentTransformers(function (OpenApiDocument $document, string $apiName) {
            $document->info->description = 'Public API for Acme.';
        })
        ->withOperationTransformers(function (Operation $operation, DiscoveredRoute $route) {
            if (in_array('throttle:api', $route->middleware, true)) {
                $operation->responses['429'] = ['description' => 'Too many requests'];
            }
        })
        ->withRuleTransformers(Ipv4CidrRule::class)
        ->withExceptionResponses(PaymentRequiredResponse::class);

    Geni::configure('v2')
        ->afterOpenApiGenerated(fn (OpenApiDocument $document) => $document->info->title .= ' (beta)');
}
```

| Method | Registers | Accepts |
| - | - | - |
| `withDocumentTransformers(...)` | Changes to the finished document | Instances, class names, closures |
| `afterOpenApiGenerated(Closure)` | Alias of `withDocumentTransformers()` | A closure |
| `withOperationTransformers(...)` | Changes to each operation | Instances, class names, closures |
| `withRuleTransformers(...)` | Custom validation rule mappings | Instances, class names |
| `withExceptionResponses(...)` | Exception to status code mappings | Instances, class names |

Each `with*()` method accepts several arguments or a single array. Class names are resolved from the service container.

### Per-API Registration

`Geni::configure()` targets the `default` API. Registrations for `default` **also apply to every named API** in `geni.apis`. Pass a name to target one API only:

```php theme={null}
Geni::configure()->withDocumentTransformers(fn (OpenApiDocument $document) => $document->info->description = 'shared');
Geni::configure('v2')->withDocumentTransformers(fn (OpenApiDocument $document) => $document->info->title .= ' (billing)');
```

Here `v2` gets both changes, while the default document only gets the shared description.

For each API, extensions run in this order:

1. Classes in `geni.extensions`
2. Classes in `geni.apis.{name}.extensions`
3. `Geni::configure()` registrations for `default`
4. `Geni::configure('{name}')` registrations

## Document Transformers

A document transformer changes the finished OpenAPI document of an API (info, servers, security, components, paths) before it is cached, served or exported.

```php theme={null}
namespace Geni\Laravel\Contracts;

use Geni\Inference\Document\OpenApiDocument;

interface DocumentTransformer
{
    public function handle(OpenApiDocument $document, string $apiName): void;
}
```

```php app/Docs/SetTermsOfService.php theme={null}
use Geni\Inference\Document\OpenApiDocument;
use Geni\Laravel\Contracts\DocumentTransformer;

class SetTermsOfService implements DocumentTransformer
{
    public function handle(OpenApiDocument $document, string $apiName): void
    {
        $document->info->termsOfService = 'https://example.com/terms';
    }
}
```

A closure receives the same arguments: `fn (OpenApiDocument $document, string $apiName) => ...`.

## Operation Transformers

An operation transformer changes one documented operation, with access to the route it came from.

```php theme={null}
namespace Geni\Laravel\Contracts;

use Geni\Inference\Document\Operation;
use Geni\Laravel\DiscoveredRoute;

interface OperationTransformer
{
    public function handle(Operation $operation, DiscoveredRoute $route, string $apiName): void;
}
```

```php app/Docs/TagOperationsWithRouteName.php theme={null}
use Geni\Inference\Document\Operation;
use Geni\Laravel\Contracts\OperationTransformer;
use Geni\Laravel\DiscoveredRoute;

class TagOperationsWithRouteName implements OperationTransformer
{
    public function handle(Operation $operation, DiscoveredRoute $route, string $apiName): void
    {
        $operation->extensions['x-route-name'] = $route->name;
    }
}
```

`DiscoveredRoute` exposes `uri`, `methods`, `action`, `middleware` and `name`. `Operation` exposes properties such as `summary`, `description`, `tags`, `operationId`, `deprecated`, `security`, `responses` and `extensions` (keys emitted as top-level `x-*` properties of the operation).

## Rule Transformers

Map custom validation rules (for example `ipv4_cidr`, or a Rule object's string form) to JSON Schema changes:

```php theme={null}
namespace Geni\Inference;

use Geni\Inference\Document\Schema;

interface RuleTransformer
{
    public function matches(string $rule, string $field): bool;

    /**
     * @return array{schema: Schema, required?: bool, diagnostics?: list<InferenceDiagnostic>}
     */
    public function transform(string $rule, string $field, Schema $schema, string $file, int $line): array;
}
```

```php app/Docs/Ipv4CidrRule.php theme={null}
use Geni\Inference\Document\Schema;
use Geni\Inference\RuleTransformer;

class Ipv4CidrRule implements RuleTransformer
{
    public function matches(string $rule, string $field): bool
    {
        return $rule === 'ipv4_cidr';
    }

    public function transform(string $rule, string $field, Schema $schema, string $file, int $line): array
    {
        $schema->type = 'string';
        $schema->format = 'ipv4-cidr';

        return ['schema' => $schema];
    }
}
```

A field validated with `'network' => ['required', 'ipv4_cidr']` is then documented as `{"type": "string", "format": "ipv4-cidr"}`.

## Exception Responses

Map your own exceptions (declared with `@throws`) to HTTP status codes:

```php theme={null}
namespace Geni\Inference;

interface ExceptionToResponse
{
    public function matches(string $exceptionClass): bool;

    public function statusFor(string $exceptionClass): ?int;
}
```

```php app/Docs/PaymentRequiredResponse.php theme={null}
use Geni\Inference\ExceptionToResponse;

class PaymentRequiredResponse implements ExceptionToResponse
{
    public function matches(string $exceptionClass): bool
    {
        return str_ends_with($exceptionClass, 'PaymentRequiredException');
    }

    public function statusFor(string $exceptionClass): ?int
    {
        return 402;
    }
}
```

A controller method with `@throws PaymentRequiredException` then documents a `402` response.

## Registering Extensions in Config

Classes can also be listed in `config/geni.php`. Each class is registered by the interface(s) it implements (`DocumentTransformer`, `OperationTransformer`, `RuleTransformer`, `ExceptionToResponse`):

```php config/geni.php theme={null}
'extensions' => [
    App\Docs\SetTermsOfService::class,
    App\Docs\TagOperationsWithRouteName::class,
    App\Docs\Ipv4CidrRule::class,
    App\Docs\PaymentRequiredResponse::class,
],

'apis' => [
    'v2' => [
        'api_path' => 'api/v2',
        'extensions' => [App\Docs\BetaNotice::class], // v2 only
    ],
],
```

<Warning>
  A class that implements none of the four interfaces throws an `InvalidArgumentException` ("... must implement DocumentTransformer, OperationTransformer, RuleTransformer or ExceptionToResponse") when the document is generated.
</Warning>

## Servers

`geni.servers` (or `servers` inside an entry of `geni.apis`) sets the OpenAPI `servers` list as description => URL:

```php config/geni.php theme={null}
'servers' => [
    'Production' => 'https://api.example.com',
    'Local' => '/',
],
```

Relative URLs go through `url()`, so `'/'` becomes your app URL (for example `http://localhost`). Paths in the document stay absolute (for example `/api/users`), so point servers at the host, not at the API prefix.

## Operation IDs

Every operation gets an `operationId`, chosen in this order:

1. An explicit id: `#[Endpoint(operationId: ...)]` or `@operationId`.
2. The route name, without a leading `api.`: `api.users.index` becomes `users.index`.
3. The controller and method: `UserController@show` becomes `user.show`. The `Controller` suffix and the `App`, `Http`, `Api` and `Controllers` namespace parts are dropped, and the last two remaining parts are used. When that is not unique, the full controller path is used.
4. As a last resort, the id is made unique with a numeric suffix.

Generated ids are unique across the document. Closure routes without a route name get no `operationId`.

A `PATCH` documented next to `PUT` on the same route (the `update` route of `Route::apiResource()`, or `Route::match(['PUT', 'PATCH'], ...)`) gets a `_patch` suffix:

| Route | Method | `operationId` |
| - | - | - |
| `things.update` | `PUT` | `things.update` |
| `things.update` | `PATCH` | `things.update_patch` |

<Note>
  Generated ids do not rename MCP tools in the default `snake` naming mode. Set `geni.mcp.tool_naming` to `operation_id` to use them as tool names. See [Tool Naming](/guides/mcp#tool-naming).
</Note>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.