Skip to content

Commit 2aab9ab

Browse files
committed
v3x - boolean schemas
as defined in https://json-schema.org/draft/2020-12/json-schema-core#section-4.3.2 true: {} false: {not: {}} previously boolean schemas were restricted to additionalProperties and there was inconsistent support for the expanded version of boolean schemas
1 parent 72cedc9 commit 2aab9ab

File tree

9 files changed

+164
-26
lines changed

9 files changed

+164
-26
lines changed

aiopenapi3/model.py

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,9 @@ def _createAnnotations(
177177
self.root = v
178178
elif _type == "object":
179179
if (
180-
schema.additionalProperties
181-
and isinstance(schema.additionalProperties, (SchemaBase, ReferenceBase))
182-
and not schema.properties
180+
not schema.properties
181+
and schema.additionalProperties is not None
182+
and not Model.booleanFalse(schema.additionalProperties)
183183
):
184184
"""
185185
https://swagger.io/docs/specification/data-models/dictionaries/
@@ -412,7 +412,7 @@ def get_patternProperties(self_):
412412

413413
classinfo.properties["aio3_patternProperties"].default = property(mkx())
414414

415-
if not schema.additionalProperties:
415+
if Model.booleanFalse(schema.additionalProperties):
416416

417417
def mkx():
418418
def validate_patternProperties(self_):
@@ -473,21 +473,16 @@ def createConfigDict(schema: "SchemaType"):
473473
arbitrary_types_allowed_ = False
474474
extra_ = "allow"
475475

476-
if schema.additionalProperties is not None:
477-
if isinstance(schema.additionalProperties, bool):
478-
if not schema.additionalProperties:
479-
extra_ = "forbid"
480-
else:
481-
arbitrary_types_allowed_ = True
482-
elif isinstance(schema.additionalProperties, (SchemaBase, ReferenceBase)):
483-
"""
484-
we allow arbitrary types if additionalProperties has no properties
485-
"""
486-
assert schema.additionalProperties.properties is not None
487-
if len(schema.additionalProperties.properties) == 0:
488-
arbitrary_types_allowed_ = True
489-
else:
490-
raise TypeError(schema.additionalProperties)
476+
if Model.booleanFalse(schema.additionalProperties):
477+
extra_ = "forbid"
478+
elif Model.booleanTrue(schema.additionalProperties) or (
479+
isinstance(schema.additionalProperties, (SchemaBase, ReferenceBase))
480+
and len(schema.additionalProperties.properties) == 0
481+
):
482+
"""
483+
we allow arbitrary types if additionalProperties has no properties
484+
"""
485+
arbitrary_types_allowed_ = True
491486

492487
if getattr(schema, "patternProperties", None):
493488
extra_ = "allow"
@@ -671,6 +666,41 @@ def is_nullable(schema: "SchemaType") -> bool:
671666
def is_type_any(schema: "SchemaType"):
672667
return schema.type is None
673668

669+
@staticmethod
670+
def booleanTrue(schema: Optional[Union["SchemaType", bool]]) -> bool:
671+
"""
672+
ACCEPT all?
673+
:param schema:
674+
:return: True if Schema is {} or True or None
675+
"""
676+
if schema is None:
677+
return True
678+
if isinstance(schema, bool):
679+
return schema is True
680+
elif isinstance(schema, (SchemaBase, ReferenceBase)):
681+
"""matches Any - {}"""
682+
return len(schema.model_fields_set) == 0
683+
else:
684+
raise ValueError(schema)
685+
686+
@staticmethod
687+
def booleanFalse(schema: Optional[Union["SchemaType", bool]]) -> bool:
688+
"""
689+
REJECT all?
690+
:param schema:
691+
:return: True if Schema is {'not':{}} or False
692+
"""
693+
694+
if schema is None:
695+
return False
696+
if isinstance(schema, bool):
697+
return schema is False
698+
elif isinstance(schema, (SchemaBase, ReferenceBase)):
699+
"""match {'not':{}}"""
700+
return (v := getattr(schema, "not_", False)) and Model.booleanTrue(v)
701+
else:
702+
raise ValueError(schema)
703+
674704
@staticmethod
675705
def createField(schema: "SchemaType", _type=None, args=None):
676706
if args is None:

aiopenapi3/openapi.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from .base import RootBase, ReferenceBase, SchemaBase, OperationBase, DiscriminatorBase
3535
from .request import RequestBase
3636
from .v30.paths import Operation
37-
from .model import is_basemodel
37+
from .model import is_basemodel, Model
3838

3939

4040
if typing.TYPE_CHECKING:
@@ -428,6 +428,7 @@ def _init_operationindex(self, use_operation_tags: bool) -> bool:
428428
@staticmethod
429429
def _get_combined_attributes(schema):
430430
"""Combine attributes from the schema."""
431+
is_array = Model.is_type_any(schema) or Model.is_type(schema, "array")
431432
return (
432433
getattr(schema, "oneOf", []) # Swagger compat
433434
+ (
@@ -438,8 +439,9 @@ def _get_combined_attributes(schema):
438439
+ getattr(schema, "anyOf", []) # Swagger compat
439440
+ schema.allOf
440441
+ list(schema.properties.values())
441-
+ ([schema.items] if schema.type == "array" and schema.items and not isinstance(schema, list) else [])
442-
+ (schema.items if schema.type == "array" and schema.items and isinstance(schema, list) else [])
442+
+ ([schema.items] if is_array and schema.items is not None and not isinstance(schema, list) else [])
443+
+ (schema.items if is_array and schema.items is not None and isinstance(schema, list) else [])
444+
+ (getattr(schema, "prefixItems", []) or [] if is_array else [])
443445
)
444446

445447
@classmethod

aiopenapi3/v30/schemas.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class Schema(ObjectExtended, SchemaBase):
4848
not_: Optional[Union["Schema", Reference]] = Field(default=None, alias="not")
4949
items: Optional[Union["Schema", Reference]] = Field(default=None)
5050
properties: dict[str, Union["Schema", Reference]] = Field(default_factory=dict)
51-
additionalProperties: Optional[Union[bool, "Schema", Reference]] = Field(default=None)
51+
additionalProperties: Optional[Union["Schema", Reference]] = Field(default=None)
5252
description: Optional[str] = Field(default=None)
5353
format: Optional[str] = Field(default=None)
5454
default: Optional[Any] = Field(default=None)
@@ -63,6 +63,16 @@ class Schema(ObjectExtended, SchemaBase):
6363

6464
model_config = ConfigDict(extra="forbid")
6565

66+
@model_validator(mode="before")
67+
@classmethod
68+
def is_boolean_schema(cls, data: Any) -> Any:
69+
if not isinstance(data, bool):
70+
return data
71+
if data:
72+
return {}
73+
else:
74+
return {"not": {}}
75+
6676
@model_validator(mode="after")
6777
@classmethod
6878
def validate_Schema_number_type(cls, s: "Schema"):

aiopenapi3/v31/components.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class Components(ObjectExtended):
2020
.. _Components Object: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#components-object
2121
"""
2222

23-
schemas: dict[str, Union[Schema, bool]] = Field(default_factory=dict)
23+
schemas: dict[str, Union[Schema]] = Field(default_factory=dict)
2424
responses: dict[str, Union[Response, Reference]] = Field(default_factory=dict)
2525
parameters: dict[str, Union[Parameter, Reference]] = Field(default_factory=dict)
2626
examples: dict[str, Union[Example, Reference]] = Field(default_factory=dict)

aiopenapi3/v31/root.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ class Root(ObjectExtended, RootBase):
3535
externalDocs: dict[Any, Any] = Field(default_factory=dict)
3636

3737
@model_validator(mode="after")
38-
def validate_Root(cls, r: "Root"):
38+
@classmethod
39+
def validate_Root(cls, r: "Root") -> "Self":
3940
assert r.paths or r.components or r.webhooks
4041
return r
4142

aiopenapi3/v31/schemas.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ class Schema(ObjectExtended, SchemaBase):
7676
"""
7777
properties: dict[str, "Schema"] = Field(default_factory=dict)
7878
patternProperties: dict[str, "Schema"] = Field(default_factory=dict)
79-
additionalProperties: Optional[Union[bool, "Schema"]] = Field(default=None)
79+
additionalProperties: Optional["Schema"] = Field(default=None)
8080
propertyNames: Optional["Schema"] = Field(default=None)
8181

8282
"""
@@ -162,7 +162,18 @@ class Schema(ObjectExtended, SchemaBase):
162162
externalDocs: Optional[dict] = Field(default=None) # 'ExternalDocs'
163163
example: Optional[Any] = Field(default=None)
164164

165+
@model_validator(mode="before")
166+
@classmethod
167+
def is_boolean_schema(cls, data: Any) -> Any:
168+
if not isinstance(data, bool):
169+
return data
170+
if data:
171+
return {}
172+
else:
173+
return {"not": {}}
174+
165175
@model_validator(mode="after")
176+
@classmethod
166177
def validate_Schema_number_type(cls, s: "Schema"):
167178
if s.type == "integer":
168179
for i in ["minimum", "maximum"]:

tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,11 @@ def with_schema_additionalProperties_and_named_properties():
440440
yield _get_parsed_yaml("schema-additionalProperties-and-named-properties" ".yaml")
441441

442442

443+
@pytest.fixture
444+
def with_schema_boolean(openapi_version):
445+
yield _get_parsed_yaml("schema-boolean.yaml", openapi_version)
446+
447+
443448
@pytest.fixture
444449
def with_schema_empty(openapi_version):
445450
yield _get_parsed_yaml("schema-empty.yaml", openapi_version)

tests/fixtures/schema-boolean.yaml

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
openapi: 3.0.0
2+
info:
3+
version: 1.0.0
4+
title: Example
5+
license:
6+
name: MIT
7+
description: |
8+
https://github.com/swagger-api/swagger-parser/issues/1770
9+
servers:
10+
- url: http://api.example.xyz/v1
11+
paths:
12+
/person/display/{personId}:
13+
get:
14+
parameters:
15+
- name: personId
16+
in: path
17+
required: true
18+
description: The id of the person to retrieve
19+
schema:
20+
type: string
21+
operationId: list
22+
responses:
23+
'200':
24+
description: OK
25+
content:
26+
application/json:
27+
schema:
28+
$ref: "#/components/schemas/BooleanTrue"
29+
components:
30+
schemas:
31+
BooleanTrue: true
32+
ArrayWithTrueItems:
33+
type: array
34+
items: true
35+
ObjectWithTrueProperty:
36+
properties:
37+
someProp: true
38+
ObjectWithTrueAdditionalProperties:
39+
additionalProperties: true
40+
AllOfWithTrue:
41+
allOf:
42+
- true
43+
AnyOfWithTrue:
44+
anyOf:
45+
- true
46+
OneOfWithTrue:
47+
oneOf:
48+
- true
49+
NotWithTrue:
50+
not: true
51+
UnevaluatedItemsTrue:
52+
unevaluatedItems: true
53+
UnevaluatedPropertiesTrue:
54+
unevaluatedProperties: true
55+
PrefixitemsWithNoAdditionalItemsAllowed:
56+
$schema: https://json-schema.org/draft/2020-12/schema
57+
prefixItems:
58+
- {}
59+
- {}
60+
- {}
61+
items: false
62+
PrefixitemsWithBooleanSchemas:
63+
$schema: https://json-schema.org/draft/2020-12/schema
64+
prefixItems:
65+
- true
66+
- false

tests/schema_test.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,3 +741,16 @@ def test_schema_allof_oneof_combined(with_schema_allof_oneof_combined):
741741
t.model_validate({"token": "1", "cmd": "invalid", "data": {"delay": 0}})
742742
with pytest.raises(ValidationError):
743743
t.model_validate({"token": "1", "cmd": "shutdown", "data": {"delay": "invalid"}})
744+
745+
746+
def test_schema_boolean(with_schema_boolean):
747+
v = copy.deepcopy(with_schema_boolean)
748+
if v["openapi"] == "3.0.3":
749+
for i in [
750+
"PrefixitemsWithNoAdditionalItemsAllowed",
751+
"PrefixitemsWithBooleanSchemas",
752+
"UnevaluatedItemsTrue",
753+
"UnevaluatedPropertiesTrue",
754+
]:
755+
del v["components"]["schemas"][i]
756+
OpenAPI("/", v)

0 commit comments

Comments
 (0)