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.
This article is part of the code quality series:
- Markdown linter - solve formatting issue in md files
- Rector 1.0.0 🎉🎉🎉, my friend, my coach
- Docker image that provides static analysis tools for PHP
- Clean code - Linux Bash - Keep the number of function parameters as small as possible
- SQL - Formatting tool
- JSON - Online linter
- Format poorly formatted PHP code
- Python - Code Quality tools
- Git - pre-commit-hooks
- Validate your OpenAPI schema against the Belgif REST standards

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
Once the belgif-lint container is in place (covered under Installation below), checking an openapi.json file is two commands:
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/jsonopenapi.json ln 1 #/paths/items//get/responses/200/content/application/jsonopenapi.json ln 1 #/paths/items/{item_id}/get/responses/200/content/application/jsonopenapi.json ln 1 #/paths/items/{item_id}/get/responses/422/content/application/jsonopenapi.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/schemaopenapi.json ln 1 #/components/schemas/ValidationError/properties/msgopenapi.json ln 1 #/components/schemas/HTTPValidationErroropenapi.json ln 1 #/components/schemas/ValidationError/properties/typeopenapi.json ln 1 #/paths/items/{item_id}/get/parameters/1/schemaopenapi.json ln 1 #/components/schemas/ValidationError/properties/locopenapi.json ln 1 #/paths/items/{item_id}/get/parameters/0/schemaopenapi.json ln 1 #/components/schemas/ValidationErroropenapi.json ln 1 #/paths/items//get/parameters/1/schemaopenapi.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__getopenapi.json ln 1 #/paths//get -- OperationId not lowerCamelCase: read_root__getopenapi.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/0This 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.
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 integeritem_idas a path parameter and an optional string query parameterq. - An endpoint
/items/that acceptsskipandlimitquery parameters for pagination.
To run it, simply start docker compose up --build -d in your terminal:
Then surf to http://localhost:8000/docs to see your application running.
You can test it using curl or your browser:
-
Root endpoint:
user@machine: ~/yourprojectYou should see:
{"Hello":"World"} -
Item with path and query parameter:
user@machine: ~/yourprojectYou should see:
{"item_id":5,"q":"somequery"} -
Items with pagination:
user@machine: ~/yourprojectYou 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.
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 tagstags_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 refrom typing import Any, cast
from fastapi import FastAPIfrom 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.
This article is part of the code quality series:
- Markdown linter - solve formatting issue in md files
- Rector 1.0.0 🎉🎉🎉, my friend, my coach
- Docker image that provides static analysis tools for PHP
- Clean code - Linux Bash - Keep the number of function parameters as small as possible
- SQL - Formatting tool
- JSON - Online linter
- Format poorly formatted PHP code
- Python - Code Quality tools
- Git - pre-commit-hooks
- Validate your OpenAPI schema against the Belgif REST standards
Loading comments…


