<!--
  canonical-url: https://www.avonture.be/blog/belgif-api-linter
  published:     2026-05-11
  This is a static plain-Markdown mirror generated at build time.
  Visit the canonical URL above for the fully rendered page, with images and interactive components.
-->

# Validate your OpenAPI schema against the Belgif REST standards

> Validate your FastAPI OpenAPI schema against the Belgif REST standards using Docker, with step-by-step fixes for the most common linting errors.

---
![Validate your OpenAPI schema against the Belgif REST standards](/img/v2/belgif.webp)

**TL;DR**This post offers a practical guide to ensuring your FastAPI applications comply with the Belgif REST standards, a strict set of guidelines required for Belgian public sector APIs. Because FastAPI's default OpenAPI schema generation often conflicts with these rules, the author demonstrates how to set up the official Belgif linter inside an isolated Docker container to easily identify compliance errors. Furthermore, the article provides actionable, step-by-step solutions and custom Python helper functions to resolve common FastAPI linting violations—such as downgrading the OpenAPI version, removing redundant title properties, and fixing camelCase naming conventions—ensuring your API documentation passes validation smoothly.

Building APIs is relatively easy, but building *compliant* and *interoperable* APIs is an entirely different challenge. If you are working on projects for the Belgian government or public sector, you have likely encountered the **[Belgif REST standards](https://www.belgif.be/specification/rest/api-guide/)**. These comprehensive guidelines ensure that APIs across various institutions remain consistent, predictable, and easy to consume.

While modern frameworks like **FastAPI** are fantastic for rapidly building APIs and automatically generating OpenAPI documentation, their default schema generation doesn't always align perfectly with Belgif's strict linting rules.

In this article, we'll explore how to use the official Belgif OpenAPI linter via Docker and how to validate your FastAPI application against some potential errors. More importantly, we'll walk through practical, step-by-step solutions to resolve the most common linting errors caused by FastAPI's default behaviors, ensuring your API gets a clean bill of health.

*Before we dive in, if you are looking for general advice on designing robust APIs, check out my previous article:* [API REST - How to write good APIs](/blog/php-api-tips).

**Quick Jump:** [Seeing the Linter Run](#seeing-the-linter-run) | [Installation — Create a dummy application](#installation--create-a-dummy-application)

## Seeing the Linter Run

Once the `belgif-lint` container is in place (covered under Installation below), checking an `openapi.json` file is two commands:

```bash
$ curl -s http://localhost:8000/openapi.json > openapi.json
```

```bash
$ docker compose run --rm belgif-lint
```

It'll produce something like this:

```txt
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------< org.apache.maven:standalone-pom >-------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] --------------------------------[ pom ]---------------------------------
[INFO]
[INFO] --- belgif-rest-guide-validator:3.1.0:validate-openapi (default-cli) @ standalone-pom ---
[INFO] Validating following files:[/app/openapi.json]
[INFO] Found kmodule: jar:file:/root/.m2/repository/io/github/belgif/rest/guide/validator/belgif-rest-guide-validator-rules/3.1.0/belgif-rest-guide-validator-rules-3.1.0.jar!/META-INF/kmodule.xml
[INFO] Creating KieModule for artifact io.github.belgif.rest.guide.validator:belgif-rest-guide-validator-rules:3.1.0
[INFO] Artifact io.github.belgif.rest.guide.validator:belgif-rest-guide-validator-rules:3.1.0 has executable model
[INFO] Start creation of KieBase: defaultKieBase
[...]

OpenApi validation summary: 30 violations and 0 ignored violations.
[MANDATORY]    [err-problem] Each error response of each operation SHOULD have a media type "application/problem+json" 2 occurrences:
openapi.json    ln   1  #/paths/items/{item_id}/get/responses/422 -- [Operation: GET /items/{item_id}]
openapi.json    ln   1  #/paths/items//get/responses/422 -- [Operation: GET /items/]
[MANDATORY]    [evo-object] In a request or response body, if any, you MUST always return a JSON object (and not e.g. an array) as a top level data structure to support future extensibility.  5 occurrences:
openapi.json    ln   1  #/paths//get/responses/200/content/application/json
openapi.json    ln   1  #/paths/items//get/responses/200/content/application/json
openapi.json    ln   1  #/paths/items/{item_id}/get/responses/200/content/application/json
openapi.json    ln   1  #/paths/items/{item_id}/get/responses/422/content/application/json
openapi.json    ln   1  #/paths/items//get/responses/422/content/application/json
[MANDATORY]    [oas-comp]   Component names SHOULD use UpperCamelCase notation. For abbreviations as well, all letters except the first one should be lowercased. 1 occurrence:
openapi.json    ln   1  #/components/schemas/HTTPValidationError
[MANDATORY]    [oas-contra] OpenAPI 3.1 improves upon OpenAPI 3.0, but to avoid interoperability problems it SHOULD NOT be used yet because it is not yet widely supported by most tooling. 1 occurrence:
#
[MANDATORY]    [oas-descr]  The title property of a Schema MUST NOT be used. 10 occurrences:
openapi.json    ln   1  #/paths/items//get/parameters/0/schema
openapi.json    ln   1  #/components/schemas/ValidationError/properties/msg
openapi.json    ln   1  #/components/schemas/HTTPValidationError
openapi.json    ln   1  #/components/schemas/ValidationError/properties/type
openapi.json    ln   1  #/paths/items/{item_id}/get/parameters/1/schema
openapi.json    ln   1  #/components/schemas/ValidationError/properties/loc
openapi.json    ln   1  #/paths/items/{item_id}/get/parameters/0/schema
openapi.json    ln   1  #/components/schemas/ValidationError
openapi.json    ln   1  #/paths/items//get/parameters/1/schema
openapi.json    ln   1  #/components/schemas/HTTPValidationError/properties/detail
[MANDATORY]    [openapi-opid] A unique operationId MUST be specified on each operation. It SHOULD have a lowerCamelCase value following common programming naming conventions for method (function) names. 3 occurrences:
openapi.json    ln   1  #/paths/items//get -- OperationId not lowerCamelCase: read_items_items__get
openapi.json    ln   1  #/paths//get -- OperationId not lowerCamelCase: read_root__get
openapi.json    ln   1  #/paths/items/{item_id}/get -- OperationId not lowerCamelCase: read_item_items__item_id__get
[MANDATORY]    [path-param] Path parameters SHOULD use lowerCamelCase notation. 1 occurrence:
openapi.json    ln   1  #/paths/items/{item_id}/get/parameters/0
[MANDATORY]    [prb-defaul] A default Problem response SHOULD be added to each operation with media type "application/problem+json" 3 occurrences:
openapi.json    ln   1  #/paths/items/{item_id}/get -- [Operation: GET /items/{item_id}]
openapi.json    ln   1  #/paths/items//get -- [Operation: GET /items/]
openapi.json    ln   1  #/paths//get -- [Operation: GET /]
[MANDATORY]    [uri-format] The server url should follow the format [/pathPrefix]/apiName/vMajorVersion optionally prefixed by https://hostname[:port] or http://localhost[:port]. The apiName must consist of one or more path segments in lowerCamelCase. 1 occurrence:
#/servers/0
[MANDATORY]    [uri-notat]  Trailing slashes MUST NOT be used. 2 occurrences:
openapi.json    ln   1  #/paths/
openapi.json    ln   1  #/paths/items/
[RECOMMENDED]  [hlth-res]   Each REST API SHOULD expose a GET /health operation which returns the availability of the API’s functionality for its clients. 1 occurrence:
openapi.json    ln   1  #/paths
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[...]
```

> **Please refer to the official site:**
>
> From now on, please refer to <https://www.belgif.be/specification/rest/api-guide> to learn how to manage errors reported by the tool.

**Bonus - Get rid of Belgif intern warnings**

While running the Belgif linter, you may encounter a lot of warnings like below:

**warnings.txt**

```txt
[WARNING] In an eval expression [ApiFunctions.existsPathWithPathParamAfter($path.getIdentifier(), parserResult)] : '$path' comes from previous pattern 'PathDefinition'. 'parserResult' comes from previous pattern 'Parser$ParserResult'. Consider placing the constraint in the pattern and removing the eval if possible, as eval is not performance-efficient.
[WARNING] In an eval expression [ApiFunctions.isMediaTypeIncluded($contentType, Set.of("application/json"))] : '$contentType' comes from previous pattern 'String'. Consider placing the constraint in the pattern and removing the eval if possible, as eval is not performance-efficient.
[WARNING] In an eval expression [isObjectType($responseSchema, parserResult)] : '$responseSchema' comes from previous pattern 'Schema'. 'parserResult' comes from previous pattern 'Parser$ParserResult'. Consider placing the constraint in the pattern and removing the eval if possible, as eval is not performance-efficient.
[WARNING] In an eval expression [!hasItemsArray($responseSchema, parserResult)] : '$responseSchema' comes from previous pattern 'Schema'. 'parserResult' comes from previous pattern 'Parser$ParserResult'. Consider placing the constraint in the pattern and removing the eval if possible, as eval is not performance-efficient.
[WARNING] In an eval expression [ApiFunctions.existsPathWithPathParamAfter($path.getIdentifier(), parserResult)] : '$path' comes from previous pattern 'PathDefinition'. 'parserResult' comes from previous pattern 'Parser$ParserResult'. Consider placing the constraint in the pattern and removing the eval if possible, as eval is not performance-efficient.
[WARNING] In an eval expression [ApiFunctions.isMediaTypeIncluded($contentType, Set.of("application/json"))] : '$contentType' comes from previous pattern 'String'. Consider placing the constraint in the pattern and removing the eval if possible, as eval is not performance-efficient.
[WARNING] In an eval expression [isObjectType($responseSchema, parserResult)] : '$responseSchema' comes from previous pattern 'Schema'. 'parserResult' comes from previous pattern 'Parser$ParserResult'. Consider placing the constraint in the pattern and removing the eval if possible, as eval is not performance-efficient.
[WARNING] In an eval expression [hasItemsArray($responseSchema, parserResult)] : '$responseSchema' comes from previous pattern 'Schema'. 'parserResult' comes from previous pattern 'Parser$ParserResult'. Consider placing the constraint in the pattern and removing the eval if possible, as eval is not performance-efficient.
[WARNING] In an eval expression [!hasCollectionResponse($responseSchema, parserResult)] : '$responseSchema' comes from previous pattern 'Schema'. 'parserResult' comes from previous pattern 'Parser$ParserResult'. Consider placing the constraint in the pattern and removing the eval if possible, as eval is not performance-efficient.
[WARNING] In an eval expression [$schema != null] : '$schema' comes from previous pattern 'Schema'. Consider placing the constraint in the pattern and removing the eval if possible, as eval is not performance-efficient.
[WARNING] More eval warnings will be suppressed...
[WARNING] No correct line number found for: /servers/0
```

The warnings are about the linter itself (in fact, comes from a tool called `Drools`) and thus **they have nothing to do with your code**. It's just visual pollution for us; the only thing we can do is to hide them.

Look at the new file below:

**compose.yaml**

```yaml
services:
  app:
    build: .
    ports:
      - "8000:80"
    volumes:
      - ./main.py:/code/main.py

  # Belgif Validator Service
  belgif-lint:
    image: maven:3.9.9-eclipse-temurin-17-alpine
    volumes:
      - .:/app:ro
      - maven-cache:/root/.m2
    working_dir: /app
    # 1. We override the entrypoint to use a shell (required for pipes)
    entrypoint: ["/bin/sh", "-c"]
    # 2. We use 'set +e' to not close the terminal if an error is met
    #    Use style.color=always to keep ANSI colors while piping output to grep
    #    Use 2>&1 to merge STDERR and STDOUT so line ordering is kept during the pipe
    command: >
      "set +e &&
      mvn \
      io.github.belgif.rest.guide.validator:belgif-rest-guide-validator-maven-plugin:3.1.0:validate-openapi \
      -Dstyle.color=always \
      -Drest-guide-validator.files=openapi.json \
      -Drest-guide-validator.outputTypes=CONSOLE \
      2>&1 \
      | grep -v 'eval expression' \
      | grep -v 'More eval warnings will be suppressed' \
      | grep -v 'comes from previous pattern' \
      | grep -v 'No correct line number'"


volumes:
  maven-cache:
```

In short, we'll run a custom command where we'll collect both STDERR and STDOUT in just one output stream then we'll run a few `grep` commands to purge specific messages from the output (the ones we can't solve).

## Belgif standards

[Belgif REST standards](https://github.com/belgif/rest-guide-validator) are defined in <https://www.belgif.be/specification/rest/api-guide/> and *is a collaborative effort by several Belgian government institutions, originally under the G-Cloud umbrella, before moving to Belgif, the Belgian Interoperability Framework. Its goal is to improve compatibility between RESTful services offered by government institutions or any other organization adopting these guidelines.*

If you want to check if your API is compliant, you can use the [belgif-rest-guide-validator](https://github.com/belgif/rest-guide-validator) as documented in the [Tools](https://www.belgif.be/specification/rest/api-guide/#openapi-tools) section.

## Installation — Create a dummy application

If you don't have one yet, just click on the `Generate install script` below and paste the CLI into a terminal then press the `Enter` key to create the project structure in a `/tmp/fastapi` folder on your disk.

**Project: /tmp/fastapi**

> **Note:**
>
> Now, please run 'docker compose up --build -d' to
> create your API website. Wait a few and open your browser, surf to
> <http://localhost:8000/docs> to open your site and see your API OpenData documentation.

**main.py**

```python
from fastapi import FastAPI
from typing import Union

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "World"}

@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
    return {"item_id": item_id, "q": q}

@app.get("/items/")
def read_items(skip: int = 0, limit: int = 10):
    return {"skip": skip, "limit": limit}
```

**compose.yaml**

```yaml
services:
  app:
    build: .
    ports:
      - "8000:80"
    volumes:
      - ./main.py:/code/main.py
```

**Dockerfile**

```docker
# Start from an official Python image.
FROM python:3.11-slim

# Set the working directory in the container.
WORKDIR /code

# Copy the requirements file and install dependencies.
# We'll install fastapi and uvicorn (the server).
RUN pip install --no-cache-dir 'fastapi[all]'

# Copy the application code into the container.
COPY ./main.py /code/main.py

# Expose the port the app runs on.
EXPOSE 80

# Command to run the application.
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
```

The `main.py` code defines:

* A root endpoint `/` that returns a simple JSON.
* An endpoint `/items/{item_id}` that accepts an integer `item_id` as a path parameter and an optional string query parameter `q`.
* An endpoint `/items/` that accepts `skip` and `limit` query parameters for pagination.

To run it, simply start `docker compose up --build -d` in your terminal:

```bash
$ docker compose up --build -d
```

Then surf to `http://localhost:`8000`/docs` to see your application running.

You can test it using `curl` or your browser:

1. **Root endpoint:**

   ```bash
   $ curl http://localhost:8000
   ```

   You should see: `{"Hello":"World"}`

2. **Item with path and query parameter:**

   ```bash
   $ curl "http://localhost:8000/items/5?q=somequery"
   ```

   You should see: `{"item_id":5,"q":"somequery"}`

3. **Items with pagination:**

   ```bash
   $ curl "http://localhost:8000/items/?skip=0&limit=20"
   ```

   You should see: `{"skip":0,"limit":20}`

## Adding the Belgif Linter

An easy way to do this is by using a Docker container. In the code sample below, take a look at the newer version of `compose.yaml`, we've added a new service called `belgif-lint` based on `maven`.

**Project: /tmp/fastapi**

> **Note:**
>
> Now, please run 'docker compose up --build -d' to
> create your API website. Wait a few and open your browser, surf to
> <http://localhost:8000/docs> to open your site.

**main.py**

```python
from fastapi import FastAPI
from typing import Union

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "World"}

@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
    return {"item_id": item_id, "q": q}

@app.get("/items/")
def read_items(skip: int = 0, limit: int = 10):
    return {"skip": skip, "limit": limit}
```

**compose.yaml**

```yaml
services:
  app:
    build: .
    ports:
      - "8000:80"
    volumes:
      - ./main.py:/code/main.py

  # Belgif Validator Service
  belgif-lint:
    image: maven:3.9.9-eclipse-temurin-17-alpine
    volumes:
      - .:/app:ro
      - maven-cache:/root/.m2
    working_dir: /app
    command: >
      mvn io.github.belgif.rest.guide.validator:belgif-rest-guide-validator-maven-plugin:3.1.0:validate-openapi
      -Drest-guide-validator.files=openapi.json
      -Drest-guide-validator.outputTypes=CONSOLE

volumes:
  maven-cache:
```

**Dockerfile**

```docker
# Start from an official Python image.
FROM python:3.11-slim

# Set the working directory in the container.
WORKDIR /code

# Copy the requirements file and install dependencies.
# We'll install fastapi and uvicorn (the server).
RUN pip install --no-cache-dir 'fastapi[all]'

# Copy the application code into the container.
COPY ./main.py /code/main.py

# Expose the port the app runs on.
EXPOSE 80

# Command to run the application.
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
```

Once you've fired `docker compose up --build -d` again to create the `belgif-lint` container, you're ready to run the check shown at the top of this article — you just need an `openapi.json` file on disk first, which is exactly what the first command up there produces.

## FastAPI tips

> **My own experience:**
>
> When integrating belgif into a FastAPI project, I encountered several linting errors that required manual troubleshooting. The guide below outlines the solution I developed to resolve these issues. **Please note that this approach is based on my personal findings and may differ from standard best practices.**

### oas-tags - Managing tags

You'll get the `[MANDATORY]    [oas-tags]   Each tag used on an operation SHOULD also be declared in the top level tags list of the OpenAPI document, with an optional description.` error when you're using one or more tags in your endpoints and if these tags are not declared.

To solve this, you've to use the `openapi_tags` attribute like illustrated below:

**tags.py**

```python

# Define the metadata for your tags
tags_metadata: list[dict[str, str]] = [
    {
        "name": "Retrieving data",
        "description": "Operations related to fetching and querying data.",
    },
    {
        "name": "Healthcheck",
        "description": "Endpoints to monitor the health status of the application.",
    },
]

app = FastAPI(
    # ...
    openapi_tags=tags_metadata,  # belgif [oas-tags]
    # ...
)
```

### oas-contra - Don't use OpenAPI 3.1 yet

If you get `[MANDATORY]    [oas-contra] OpenAPI 3.1 improves upon OpenAPI 3.0, but to avoid interoperability problems it SHOULD NOT be used yet because it is not yet widely supported by most tooling.`, you can address this error by downgrading the OpenAPI version used by FastAPI like this:

```python
app = FastAPI(
    # ...
    openapi_version="3.0.2", # belgif [oas-contra]
)
```

### oas-descr - Title property has to be removed

You'll get the `[MANDATORY]    [oas-descr]  The title property of a Schema MUST NOT be used.` when FastAPI will generate both a `title` and a `description` property for your objects.

The solution is to add a helper function and remove the `title` (since `description` is always generated):

Partial code:

```python
if "components" in openapi_schema and "schemas" in openapi_schema["components"]:
    for schema in openapi_schema["components"]["schemas"].values():
        _remove_titles(schema)
```

See the helper provided later on in the post.

### oas-comp - Component names SHOULD use UpperCamelCase notation

If you get the `[MANDATORY]    [oas-comp]   Component names SHOULD use UpperCamelCase notation. For abbreviations as well, all letters except the first one should be lowercased.` error, you'll need to add a rename function to update e.g. `HTTPValidationError` to `HttpValidationError`.

It can be done f.i. like this (partial code):

```python
def _fix_schema_names(openapi_schema: dict[str, Any]) -> None:
    """
    Renames schemas that violate Belgif's naming conventions.

    Specifically targets 'HTTPValidationError' (abbreviations must be camel-cased
    like 'Http', not 'HTTP').
    """
    components: Any = openapi_schema.get("components", {})
    schemas: Any = components.get("schemas", {})

    # Fix: HTTPValidationError -> HttpValidationError
    if "HTTPValidationError" in schemas:
        # 1. Move the definition to the new key
        schemas["HttpValidationError"] = schemas.pop("HTTPValidationError")

        # 2. Update all references ($ref) in the entire document to point to the new key
        _update_refs(
            openapi_schema, "#/components/schemas/HTTPValidationError", "#/components/schemas/HttpValidationError"
        )
```

See the helper provided later on in the post.

### openapi-opid - lowerCamelCase for operationId

The message `[MANDATORY]    [openapi-opid] A unique operationId MUST be specified on each operation. It SHOULD have a lowerCamelCase value following common programming naming conventions for method (function) names.` notifies you about the use of a wrong syntax.

Partial code:

```python
def _fix_operation_ids(openapi_schema: dict[str, Any]) -> None:
    """
    Iterates over all paths and converts snake_case operationIds to camelCase.
    """
    paths: Any = openapi_schema.get("paths", {})

    for path_item in paths.values():
        for operation in path_item.values():
            if isinstance(operation, dict) and "operationId" in operation:
                old_id: str = cast(str, operation["operationId"])
                operation["operationId"] = _to_camel_case(old_id)
```

### The openapi.py helper

The helper provided below can help you to solve `oas-descr` and `oas-comp` errors.

The `main.py` file illustrates how to call the helper.

**main.py**

```python
from helpers.openapi import configure_custom_openapi

# Your code...

app = FastAPI(
    # ...
)

# Call the helper and pass the FastAPI object

configure_custom_openapi(app)
```

**helpers/openapi.py**

```python
"""
OpenAPI Configuration Helper.

This module isolates the logic required to customize the generated OpenAPI (Swagger)
schema (i.e. the "openapi.json" file).

It specifically handles:
1. Removal of redundant 'title' fields (Rule: oas-descr).
2. Renaming schemas to strict CamelCase (e.g. HTTPValidationError -> HttpValidationError).
3. Converting Operation IDs to camelCase (Rule: openapi-opid).

This allows 'main.py' to remain clean while satisfying strict linters like Belgif.
"""

import re
from typing import Any, cast

from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi

# Define a recursive type for JSON schemas to fix strict typing issues.
JsonNode = dict[str, Any] | list[Any] | Any


def _remove_titles(schema: Any) -> None:
    """
    Recursively removes the 'title' key from a JSON-like schema dictionary.
    """
    if isinstance(schema, dict):
        # Explicit cast to silence linter warnings about unknown keys
        s_dict: dict[str, Any] = cast(dict[str, Any], schema)

        s_dict.pop("title", None)

        for value in s_dict.values():
            _remove_titles(value)

    elif isinstance(schema, list):
        for item in schema:
            _remove_titles(item)


def _to_camel_case(snake_str: str) -> str:
    """Converts snake_case to lowerCamelCase."""
    components: list[str] = snake_str.split("_")
    # We capitalize the first letter of each component except the first one
    return components[0] + "".join(x.title() for x in components[1:])


def _update_refs(item: Any, old_ref: str, new_ref: str) -> None:
    """
    Recursively updates $ref strings in the schema.

    Used when a schema component is renamed.
    """
    if isinstance(item, dict):
        s_dict: dict[str, Any] = cast(dict[str, Any], item)

        # If this node is a reference to the old name, update it
        if s_dict.get("$ref") == old_ref:
            s_dict["$ref"] = new_ref
            return

        for value in s_dict.values():
            _update_refs(value, old_ref, new_ref)

    elif isinstance(item, list):
        for sub_item in item:
            _update_refs(sub_item, old_ref, new_ref)


def _fix_schema_names(openapi_schema: dict[str, Any]) -> None:
    """
    Renames schemas that violate Belgif's naming conventions.

    Specifically targets 'HTTPValidationError' (abbreviations must be camel-cased
    like 'Http', not 'HTTP').
    """
    components: Any = openapi_schema.get("components", {})
    schemas: Any = components.get("schemas", {})

    # Fix: HTTPValidationError -> HttpValidationError
    if "HTTPValidationError" in schemas:
        # 1. Move the definition to the new key
        schemas["HttpValidationError"] = schemas.pop("HTTPValidationError")

        # 2. Update all references ($ref) in the entire document to point to the new key
        _update_refs(
            openapi_schema, "#/components/schemas/HTTPValidationError", "#/components/schemas/HttpValidationError"
        )


def _fix_operation_ids(openapi_schema: dict[str, Any]) -> None:
    """
    Iterates over all paths and converts snake_case operationIds to camelCase.
    """
    paths: Any = openapi_schema.get("paths", {})

    for path_item in paths.values():
        for operation in path_item.values():
            if isinstance(operation, dict) and "operationId" in operation:
                old_id: str = cast(str, operation["operationId"])
                operation["operationId"] = _to_camel_case(old_id)


def configure_custom_openapi(app: FastAPI) -> None:
    """
    Overrides the default OpenAPI generator to apply Belgif compliance fixes.

    Args:
        app: The FastAPI application instance to configure.
    """

    def custom_openapi() -> dict[str, Any]:
        if app.openapi_schema:
            return app.openapi_schema

        # 1. Generate the default OpenAPI schema
        openapi_schema: dict[str, Any] = get_openapi(
            title=app.title,
            version=app.version,
            description=app.description,
            openapi_version=app.openapi_version,
            routes=app.routes,
            tags=app.openapi_tags,
        )

        # 2. Fix 1: Remove Titles (Clutter reduction) (belgif [oas-descr])
        if "components" in openapi_schema and "schemas" in openapi_schema["components"]:
            for schema in openapi_schema["components"]["schemas"].values():
                _remove_titles(schema)

        # 3. Fix 2: Rename Schemas (Naming conventions) (belgif [oas-comp])
        _fix_schema_names(openapi_schema)

        # 4. Fix 3: Operation IDs (snake_case -> camelCase) (belgif [openapi-opid])
        _fix_operation_ids(openapi_schema)

        app.openapi_schema = openapi_schema
        return app.openapi_schema

    # Assign the custom function to the app instance
    app.openapi = custom_openapi  # type: ignore[method-assign]
```

## Conclusion

FastAPI gets you a working OpenAPI schema for free, but "working" and "Belgif-compliant" are two different bars — the linter above is what closes that gap, and the fixes in this article cover the specific violations FastAPI's defaults trigger most often (tags, the OpenAPI version, `title` properties, naming conventions). Run `belgif-lint` before you ship, not after a reviewer flags it.

If you're still shaping the API itself rather than validating it, [API REST - How to write good APIs](/blog/php-api-tips) is the article to read first.
