<!--
  canonical-url: https://www.avonture.be/fr/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.
-->

# Validez votre schéma OpenAPI face aux standards REST Belgif

> Validez le schéma OpenAPI de votre FastAPI face aux standards REST Belgif avec Docker, avec des corrections pas à pas pour les erreurs de linting les plus courantes.

---
![Validez votre schéma OpenAPI face aux standards REST Belgif](/img/v2/belgif.webp)

**TL;DR**Cet article est un guide pratique pour rendre vos applications FastAPI conformes aux standards REST Belgif, un ensemble de règles strictes imposé aux API du secteur public belge. Comme la génération par défaut du schéma OpenAPI de FastAPI entre souvent en conflit avec ces règles, l'auteur montre comment mettre en place le linter officiel Belgif dans un container Docker isolé pour repérer facilement les erreurs de conformité. L'article fournit aussi des solutions concrètes, pas à pas, et des fonctions Python utilitaires pour résoudre les violations les plus fréquentes — rétrograder la version d'OpenAPI, supprimer les propriétés `title` redondantes, corriger les conventions de nommage camelCase — afin que la documentation de votre API passe la validation sans accroc.

Construire des API est relativement facile, mais construire des API *conformes* et *interopérables* est un tout autre défi. Si vous travaillez sur des projets pour le gouvernement belge ou le secteur public, vous avez probablement déjà croisé les **[standards REST Belgif](https://www.belgif.be/specification/rest/api-guide/)**. Ces directives complètes garantissent que les API des différentes institutions restent cohérentes, prévisibles et faciles à consommer.

Les frameworks modernes comme **FastAPI** sont excellents pour construire rapidement des API et générer automatiquement la documentation OpenAPI, mais leur génération de schéma par défaut ne colle pas toujours parfaitement aux règles de linting strictes de Belgif.

Dans cet article, nous allons voir comment utiliser le linter OpenAPI officiel de Belgif via Docker et comment valider votre application FastAPI face à quelques erreurs potentielles. Plus important encore, nous allons parcourir des solutions pratiques, pas à pas, pour résoudre les erreurs de linting les plus courantes causées par les comportements par défaut de FastAPI, afin que votre API obtienne un certificat de bonne santé.

*Avant d'entrer dans le vif du sujet, si vous cherchez des conseils généraux sur la conception d'API robustes, jetez un œil à mon article précédent :* [API REST - How to write good APIs](/blog/php-api-tips).

**Quick Jump:** [Le linter en action](#seeing-the-linter-run) | [Installation — Créer une application bidon](#installation--create-a-dummy-application)

## Le linter en action {#seeing-the-linter-run}

Une fois le container `belgif-lint` en place (voir la section Installation ci-dessous), vérifier un fichier `openapi.json` tient en deux commandes :

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

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

Cela produira quelque chose comme ceci :

```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] ------------------------------------------------------------------------
[...]
```

> **Référez-vous au site officiel:**
>
> À partir de maintenant, référez-vous à <https://www.belgif.be/specification/rest/api-guide> pour apprendre à gérer les erreurs signalées par l'outil.

**Bonus - Se débarrasser des warnings internes de Belgif**

En exécutant le linter Belgif, vous allez peut-être rencontrer de nombreux warnings comme ceux-ci :

**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
```

Ces warnings concernent le linter lui-même (ils viennent en fait d'un outil appelé `Drools`) et donc **ils n'ont rien à voir avec votre code**. C'est juste de la pollution visuelle pour nous ; la seule chose que nous pouvons faire, c'est les masquer.

Regardez le nouveau fichier ci-dessous :

**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:
```

En résumé, nous allons lancer une commande personnalisée où nous récupérons STDERR et STDOUT dans un seul stream de sortie, puis nous exécutons quelques commandes `grep` pour purger certains messages de la sortie (ceux que nous ne pouvons pas résoudre).

## Les standards Belgif {#belgif-standards}

Les [standards REST Belgif](https://github.com/belgif/rest-guide-validator) sont définis sur <https://www.belgif.be/specification/rest/api-guide/> et constituent *un effort collaboratif de plusieurs institutions gouvernementales belges, à l'origine sous l'égide du G-Cloud, avant de passer chez Belgif, le Belgian Interoperability Framework. Son objectif est d'améliorer la compatibilité entre les services RESTful proposés par les institutions publiques ou toute autre organisation adoptant ces directives.*

Si vous voulez vérifier si votre API est conforme, vous pouvez utiliser le [belgif-rest-guide-validator](https://github.com/belgif/rest-guide-validator) comme documenté dans la section [Tools](https://www.belgif.be/specification/rest/api-guide/#openapi-tools).

## Installation — Créer une application bidon {#installation--create-a-dummy-application}

Si vous n'en avez pas encore, cliquez simplement sur `Generate install script` ci-dessous et collez la ligne de commande dans un terminal, puis appuyez sur la touche `Enter` pour créer la structure du projet dans un dossier `/tmp/fastapi` sur votre disque.

**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"]
```

Le code de `main.py` définit :

* Un endpoint racine `/` qui retourne un simple JSON.
* Un endpoint `/items/{item_id}` qui accepte un entier `item_id` comme paramètre de path et un paramètre de query optionnel `q` de type chaîne.
* Un endpoint `/items/` qui accepte les paramètres de query `skip` et `limit` pour la pagination.

Pour le lancer, exécutez simplement `docker compose up --build -d` dans votre terminal :

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

Rendez-vous ensuite sur `http://localhost:`8000`/docs` pour voir votre application tourner.

Vous pouvez la tester avec `curl` ou votre navigateur :

1. **Endpoint racine :**

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

   Vous devriez voir : `{"Hello":"World"}`

2. **Item avec paramètre de path et de query :**

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

   Vous devriez voir : `{"item_id":5,"q":"somequery"}`

3. **Items avec pagination :**

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

   Vous devriez voir : `{"skip":0,"limit":20}`

## Ajouter le linter Belgif {#adding-the-belgif-linter}

Le plus simple est d'utiliser un container Docker. Dans l'exemple de code ci-dessous, regardez la nouvelle version de `compose.yaml` : nous avons ajouté un service `belgif-lint` basé sur `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"]
```

Une fois que vous avez relancé `docker compose up --build -d` pour créer le container `belgif-lint`, vous êtes prêt à lancer la vérification montrée en début d'article — il vous faut juste d'abord un fichier `openapi.json` sur le disque, ce que produit exactement la première commande là-haut.

## Astuces FastAPI {#fastapi-tips}

> **Mon expérience personnelle:**
>
> En intégrant belgif dans un projet FastAPI, j'ai rencontré plusieurs erreurs de linting qui ont demandé du dépannage manuel. Le guide ci-dessous décrit la solution que j'ai mise au point pour les résoudre. **Notez que cette approche repose sur mes propres constats et peut différer des bonnes pratiques standard.**

### oas-tags - Gérer les tags {#oas-tags---managing-tags}

Vous obtiendrez l'erreur `[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.` lorsque vous utilisez un ou plusieurs tags dans vos endpoints et que ces tags ne sont pas déclarés.

Pour résoudre ça, vous devez utiliser l'attribut `openapi_tags` comme illustré ci-dessous :

**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 - N'utilisez pas encore OpenAPI 3.1 {#oas-contra---dont-use-openapi-31-yet}

Si vous obtenez `[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.`, vous pouvez régler cette erreur en rétrogradant la version d'OpenAPI utilisée par FastAPI comme ceci :

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

### oas-descr - La propriété title doit être supprimée {#oas-descr---title-property-has-to-be-removed}

Vous obtiendrez `[MANDATORY]    [oas-descr]  The title property of a Schema MUST NOT be used.` quand FastAPI génère à la fois une propriété `title` et une propriété `description` pour vos objets.

La solution est d'ajouter une fonction utilitaire et de supprimer le `title` (puisque `description` est toujours généré) :

Code partiel :

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

Voir l'utilitaire fourni plus loin dans l'article.

### oas-comp - Les noms de composants doivent utiliser la notation UpperCamelCase {#oas-comp---component-names-should-use-uppercamelcase-notation}

Si vous obtenez l'erreur `[MANDATORY]    [oas-comp]   Component names SHOULD use UpperCamelCase notation. For abbreviations as well, all letters except the first one should be lowercased.`, vous devrez ajouter une fonction de renommage pour transformer par exemple `HTTPValidationError` en `HttpValidationError`.

Cela peut se faire par exemple comme ceci (code partiel) :

```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"
        )
```

Voir l'utilitaire fourni plus loin dans l'article.

### openapi-opid - lowerCamelCase pour operationId {#openapi-opid---lowercamelcase-for-operationid}

Le 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.` vous signale l'utilisation d'une mauvaise syntaxe.

Code partiel :

```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)
```

### L'utilitaire openapi.py {#the-openapipy-helper}

L'utilitaire fourni ci-dessous peut vous aider à résoudre les erreurs `oas-descr` et `oas-comp`.

Le fichier `main.py` montre comment appeler cet utilitaire.

**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 {#conclusion}

FastAPI vous offre gratuitement un schéma OpenAPI fonctionnel, mais « fonctionnel » et « conforme Belgif » sont deux niveaux d'exigence différents — le linter ci-dessus est ce qui comble l'écart, et les corrections de cet article couvrent les violations que les réglages par défaut de FastAPI déclenchent le plus souvent (les tags, la version d'OpenAPI, les propriétés `title`, les conventions de nommage). Lancez `belgif-lint` avant de livrer, pas après qu'un relecteur l'ait signalé.

Si vous êtes encore en train de concevoir l'API elle-même plutôt que de la valider, [API REST - How to write good APIs](/blog/php-api-tips) est l'article à lire en premier.
