Skip to main content

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.

· 9 min read
View raw
Christophe
Markdown, WSL and Docker lover ~ Insatiable curious.
Cet article existe aussi en français — lire la version française.

Validate your OpenAPI schema against the Belgif REST standards

⚡
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. 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.

Seeing the Linter Run​

Your values for this page
default
Applies to the commands on this pageSaved on this device only

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

>user@machine: ~/yourproject
>user@machine: ~/yourproject

It'll produce something like this:

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

This is a Maven build log showing the output of running an OpenAPI validation tool called belgif-rest-guide-validator. The tool scanned an OpenAPI specification file (openapi.json) and found 30 violations of Belgian API design guidelines. The violations range from formatting issues (like naming conventions not being followed) to structural problems (like missing required response types and default error handlers), and the build ultimately failed because these violations need to be fixed.

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:

[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

This is a compiler warning report showing that a pattern-matching rule system is using inefficient 'eval' expressions to filter results after matching. Instead of checking conditions like 'is this media type JSON?' or 'does this schema have an items array?' as part of the pattern match itself, the system is matching first and then checking these conditions afterwards, which is slower. The warnings repeat because similar inefficient patterns appear multiple times in the codebase.

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:

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:

This is a Docker Compose configuration that defines two services: an app service built from the current directory that exposes port 8000, and a belgif-lint service that runs Maven to validate an OpenAPI specification file. The belgif-lint service uses a shell to run a Maven command while filtering out noisy warning messages from the output.

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 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 as documented in the 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 setup: /tmp/fastapi
Help

Need help here? Click the "Help" button above for setup instructions.

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:

>user@machine: ~/yourproject

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

You can test it using curl or your browser:

  1. Root endpoint:

    >user@machine: ~/yourproject

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

  2. Item with path and query parameter:

    >user@machine: ~/yourproject

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

  3. Items with pagination:

    >user@machine: ~/yourproject

    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 setup: /tmp/fastapi
Help

Need help here? Click the "Help" button above for setup instructions.

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:

# 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]
# ...
)

This code creates a list of metadata describing API endpoint categories and passes it to FastAPI to organize endpoints in the API documentation. Each category has a name and description that will appear in the auto-generated API docs.

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:

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:

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

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:

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.

from helpers.openapi import configure_custom_openapi
# Your code...
app = FastAPI(
# ...
)
# Call the helper and pass the FastAPI object
configure_custom_openapi(app)

This code imports a helper function that customizes OpenAPI documentation for a FastAPI application, then calls that function and passes the FastAPI app object to it so the documentation gets configured with whatever custom settings are defined in that helper.

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

This module customizes FastAPI's automatically generated OpenAPI documentation to comply with Belgif linting rules. It removes unnecessary 'title' fields, renames schema components to proper CamelCase (like HTTPValidationError → HttpValidationError), and converts operation IDs from snake_case to camelCase. The main entry point replaces FastAPI's default OpenAPI generator with a custom one that applies these three fixes in sequence.

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 is the article to read first.


🦋 Join the conversation on BlueskyRequires a free Bluesky account to like, repost or comment. This block talks to Bluesky's public API directly from your browser (no cookies, no sign-in) — that means your IP address reaches Bluesky's servers just by this section loading.

Loading comments…

Was this article helpful?
Did you try the steps in this article?