feat(api): add OpenAPI schema postprocessing hooks for Mintlify compatibility

Add three postprocessing hooks to fix OpenAPI 3.0.x compatibility issues
generated by drf-spectacular-jsonapi:

1. fix_empty_id_fields: Fixes empty id field definitions ({}) in JSON:API
   request schemas (particularly PATCH/update requests) by replacing them
   with proper schema: {"type": "string", "format": "uuid", "description": "..."}

2. fix_pattern_properties: Converts patternProperties to additionalProperties
   for OpenAPI 3.0 compatibility. patternProperties is only available in
   OpenAPI 3.1+, but drf-spectacular generates 3.0.3 specs. This is needed
   for the Prowler mutelist configuration which uses dynamic keys.

3. fix_type_formats: Fixes invalid type values like "email" that
   drf-spectacular generates from Django's EmailField. Converts them to
   proper OpenAPI format: "type": "string" with "format": "email".
   Also handles "url" → "uri" and "uuid" formats.

These hooks ensure the generated OpenAPI schema validates correctly with
Mintlify and other OpenAPI 3.0.x tools.
This commit is contained in:
Andoni A.
2025-11-05 11:36:24 +01:00
parent 4037927c61
commit 00441e776d
5 changed files with 44847 additions and 0 deletions
+116
View File
@@ -36,6 +36,122 @@ def _extract_task_example_from_components(components):
}
def fix_empty_id_fields(result, generator, request, public): # noqa: F841
"""
Fix empty id fields in JSON:API request schemas.
drf-spectacular-jsonapi sometimes generates empty id field definitions ({})
which cause validation errors in Mintlify and other OpenAPI validators.
"""
if not isinstance(result, dict):
return result
components = result.get("components", {}) or {}
schemas = components.get("schemas", {}) or {}
for schema_name, schema in schemas.items():
if not isinstance(schema, dict):
continue
# Check if this is a JSON:API request schema with a data object
properties = schema.get("properties", {})
if not isinstance(properties, dict):
continue
data_prop = properties.get("data")
if not isinstance(data_prop, dict):
continue
data_properties = data_prop.get("properties", {})
if not isinstance(data_properties, dict):
continue
# Fix empty id field
id_field = data_properties.get("id")
if id_field == {} or (isinstance(id_field, dict) and not id_field):
data_properties["id"] = {
"type": "string",
"format": "uuid",
"description": "Unique identifier for this resource object.",
}
return result
def convert_pattern_properties_to_additional(obj):
"""
Recursively convert patternProperties to additionalProperties.
OpenAPI 3.0.x doesn't support patternProperties (only available in 3.1+).
"""
if isinstance(obj, dict):
if "patternProperties" in obj:
# Get the pattern and its schema
pattern_props = obj.pop("patternProperties")
# Use the first pattern's schema as additionalProperties
if pattern_props:
first_pattern_schema = next(iter(pattern_props.values()))
obj["additionalProperties"] = first_pattern_schema
# Recursively process all nested objects
for key, value in obj.items():
obj[key] = convert_pattern_properties_to_additional(value)
elif isinstance(obj, list):
return [convert_pattern_properties_to_additional(item) for item in obj]
return obj
def fix_pattern_properties(result, generator, request, public): # noqa: F841
"""
Convert patternProperties to additionalProperties for OpenAPI 3.0 compatibility.
patternProperties is only supported in OpenAPI 3.1+, but drf-spectacular
generates OpenAPI 3.0.x specs.
"""
if not isinstance(result, dict):
return result
return convert_pattern_properties_to_additional(result)
def fix_invalid_types(obj):
"""
Recursively fix invalid type values in OpenAPI schemas.
Converts invalid types like "email" to proper OpenAPI format.
"""
if isinstance(obj, dict):
# Fix invalid "type" values
if "type" in obj:
type_value = obj["type"]
if type_value == "email":
obj["type"] = "string"
obj["format"] = "email"
elif type_value == "url":
obj["type"] = "string"
obj["format"] = "uri"
elif type_value == "uuid":
obj["type"] = "string"
obj["format"] = "uuid"
# Recursively process all nested objects
for key, value in list(obj.items()):
obj[key] = fix_invalid_types(value)
elif isinstance(obj, list):
return [fix_invalid_types(item) for item in obj]
return obj
def fix_type_formats(result, generator, request, public): # noqa: F841
"""
Fix invalid type values in OpenAPI schemas.
drf-spectacular sometimes generates invalid type values like "email"
instead of "type": "string" with "format": "email".
"""
if not isinstance(result, dict):
return result
return fix_invalid_types(result)
def attach_task_202_examples(result, generator, request, public): # noqa: F841
if not isinstance(result, dict):
return result
+3
View File
@@ -125,6 +125,9 @@ SPECTACULAR_SETTINGS = {
"drf_spectacular_jsonapi.hooks.fix_nested_path_parameters",
],
"POSTPROCESSING_HOOKS": [
"api.schema_hooks.fix_empty_id_fields",
"api.schema_hooks.fix_pattern_properties",
"api.schema_hooks.fix_type_formats",
"api.schema_hooks.attach_task_202_examples",
],
"TITLE": "API Reference - Prowler",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4
View File
@@ -329,6 +329,10 @@
{
"tab": "Public Roadmap",
"href": "https://roadmap.prowler.com/"
},
{
"tab": "API Reference",
"openapi": "api-reference/openapi.json"
}
],
"global": {