Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OAS 3.1 #825

Merged
merged 1 commit into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion drf_spectacular/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,14 @@ def extract_hash(schema):
if '' in prop_enum_original_list:
components.append(create_enum_component('BlankEnum', schema={'enum': ['']}))
if None in prop_enum_original_list:
components.append(create_enum_component('NullEnum', schema={'enum': [None]}))
if spectacular_settings.OAS_VERSION.startswith('3.1'):
components.append(create_enum_component('NullEnum', schema={'type': 'null'}))
else:
components.append(create_enum_component('NullEnum', schema={'enum': [None]}))

# undo OAS 3.1 type list NULL construction as we cover this in a separate component already
if spectacular_settings.OAS_VERSION.startswith('3.1') and isinstance(enum_schema['type'], list):
enum_schema['type'] = [t for t in enum_schema['type'] if t != 'null'][0]

if len(components) == 1:
prop_schema.update(components[0].ref)
Expand Down
1 change: 1 addition & 0 deletions drf_spectacular/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,7 @@ def _get_serializer_field_meta(self, field, direction):
if field.write_only:
meta['writeOnly'] = True
if field.allow_null:
# this will be converted later in case of OAS 3.1
meta['nullable'] = True
if isinstance(field, serializers.CharField) and not field.allow_blank:
# blank check only applies to inbound requests
Expand Down
20 changes: 19 additions & 1 deletion drf_spectacular/plumbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ def build_root_object(paths, components, version):
else:
version = settings.VERSION or version or ''
root = {
'openapi': '3.0.3',
'openapi': settings.OAS_VERSION,
'info': {
'title': settings.TITLE,
'version': version,
Expand Down Expand Up @@ -511,6 +511,24 @@ def safe_ref(schema):


def append_meta(schema, meta):
if spectacular_settings.OAS_VERSION.startswith('3.1'):
schema_nullable = meta.pop('nullable', None)
meta_nullable = schema.pop('nullable', None)

if schema_nullable or meta_nullable:
if 'type' in schema:
schema['type'] = [schema['type'], 'null']
elif '$ref' in schema:
schema = {'oneOf': [schema, {'type': 'null'}]}
else:
assert False, 'Invalid nullable case' # pragma: no cover

# these two aspects were merged in OpenAPI 3.1
if "exclusiveMinimum" in schema and "minimum" in schema:
schema["exclusiveMinimum"] = schema.pop("minimum")
if "exclusiveMaximum" in schema and "maximum" in schema:
schema["exclusiveMaximum"] = schema.pop("maximum")

return safe_ref({**schema, **meta})


Expand Down
5 changes: 5 additions & 0 deletions drf_spectacular/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
# accurately modeled when request and response components are separated.
'ENFORCE_NON_BLANK_FIELDS': False,

# This version string will end up the in schema header. The default OpenAPI
# version is 3.0.3, which is heavily tested. We now also support 3.1.0,
# which contains the same features and a few mandatory, but minor changes.
'OAS_VERSION': '3.0.3',

# Configuration for serving a schema subset with SpectacularAPIView
'SERVE_URLCONF': None,
# complete public schema or a subset based on the requesting user
Expand Down
17 changes: 13 additions & 4 deletions drf_spectacular/validation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,28 @@

import jsonschema

JSON_SCHEMA_SPEC_PATH = os.path.join(os.path.dirname(__file__), 'openapi3_schema.json')


def validate_schema(api_schema):
"""
Validate generated API schema against OpenAPI 3.0.X json schema specification.
Note: On conflict, the written specification always wins over the json schema.

OpenApi3 schema specification taken from:

https://github.com/OAI/OpenAPI-Specification/blob/master/schemas/v3.0/schema.json
https://github.com/OAI/OpenAPI-Specification/blob/6d17b631fff35186c495b9e7d340222e19d60a71/schemas/v3.0/schema.json
https://github.com/OAI/OpenAPI-Specification/blob/9dff244e5708fbe16e768738f4f17cf3fddf4066/schemas/v3.0/schema.json

https://github.com/OAI/OpenAPI-Specification/blob/main/schemas/v3.1/schema.json
https://github.com/OAI/OpenAPI-Specification/blob/9dff244e5708fbe16e768738f4f17cf3fddf4066/schemas/v3.1/schema.json
"""
with open(JSON_SCHEMA_SPEC_PATH) as fh:
if api_schema['openapi'].startswith("3.0"):
schema_spec_path = os.path.join(os.path.dirname(__file__), 'openapi_3_0_schema.json')
elif api_schema['openapi'].startswith("3.1"):
schema_spec_path = os.path.join(os.path.dirname(__file__), 'openapi_3_1_schema.json')
else:
raise RuntimeError('No validation specification available') # pragma: no cover

with open(schema_spec_path) as fh:
openapi3_schema_spec = json.load(fh)

# coerce any remnants of objects to basic types
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "https://spec.openapis.org/oas/3.0/schema/2019-04-02",
"id": "https://spec.openapis.org/oas/3.0/schema/2021-09-28",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Validation schema for OpenAPI Specification 3.0.X.",
"description": "The description of OpenAPI v3.0.x documents, as defined by https://spec.openapis.org/oas/v3.0.3",
"type": "object",
"required": [
"openapi",
Expand Down Expand Up @@ -1358,9 +1358,8 @@
"description": "Bearer",
"properties": {
"scheme": {
"enum": [
"bearer"
]
"type": "string",
"pattern": "^[Bb][Ee][Aa][Rr][Ee][Rr]$"
}
}
},
Expand All @@ -1374,9 +1373,8 @@
"properties": {
"scheme": {
"not": {
"enum": [
"bearer"
]
"type": "string",
"pattern": "^[Bb][Ee][Aa][Rr][Ee][Rr]$"
}
}
}
Expand Down Expand Up @@ -1489,7 +1487,8 @@
"PasswordOAuthFlow": {
"type": "object",
"required": [
"tokenUrl"
"tokenUrl",
"scopes"
],
"properties": {
"tokenUrl": {
Expand All @@ -1516,7 +1515,8 @@
"ClientCredentialsFlow": {
"type": "object",
"required": [
"tokenUrl"
"tokenUrl",
"scopes"
],
"properties": {
"tokenUrl": {
Expand Down Expand Up @@ -1544,7 +1544,8 @@
"type": "object",
"required": [
"authorizationUrl",
"tokenUrl"
"tokenUrl",
"scopes"
],
"properties": {
"authorizationUrl": {
Expand Down Expand Up @@ -1628,7 +1629,14 @@
"headers": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/Header"
"oneOf": [
{
"$ref": "#/definitions/Header"
},
{
"$ref": "#/definitions/Reference"
}
]
}
},
"style": {
Expand All @@ -1648,6 +1656,10 @@
"default": false
}
},
"patternProperties": {
"^x-": {
}
},
"additionalProperties": false
}
}
Expand Down
Loading