Skip to content

Commit 07e1dea

Browse files
authored
🐛 Fix JSON Schema accepting bools as valid JSON Schemas, e.g. additionalProperties: false (fastapi#9781)
* 🐛 Fix JSON Schema accepting bools as valid JSON Schemas, e.g. additionalProperties: false * ✅ Add test to ensure additionalProperties can be false * ♻️ Tweak OpenAPI models to support Pydantic v1's JSON Schema for tuples
1 parent 0f105d9 commit 07e1dea

File tree

2 files changed

+142
-19
lines changed

2 files changed

+142
-19
lines changed

fastapi/openapi/models.py

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -114,27 +114,30 @@ class Schema(BaseModel):
114114
dynamicAnchor: Optional[str] = Field(default=None, alias="$dynamicAnchor")
115115
ref: Optional[str] = Field(default=None, alias="$ref")
116116
dynamicRef: Optional[str] = Field(default=None, alias="$dynamicRef")
117-
defs: Optional[Dict[str, "Schema"]] = Field(default=None, alias="$defs")
117+
defs: Optional[Dict[str, "SchemaOrBool"]] = Field(default=None, alias="$defs")
118118
comment: Optional[str] = Field(default=None, alias="$comment")
119119
# Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-a-vocabulary-for-applying-s
120120
# A Vocabulary for Applying Subschemas
121-
allOf: Optional[List["Schema"]] = None
122-
anyOf: Optional[List["Schema"]] = None
123-
oneOf: Optional[List["Schema"]] = None
124-
not_: Optional["Schema"] = Field(default=None, alias="not")
125-
if_: Optional["Schema"] = Field(default=None, alias="if")
126-
then: Optional["Schema"] = None
127-
else_: Optional["Schema"] = Field(default=None, alias="else")
128-
dependentSchemas: Optional[Dict[str, "Schema"]] = None
129-
prefixItems: Optional[List["Schema"]] = None
130-
items: Optional[Union["Schema", List["Schema"]]] = None
131-
contains: Optional["Schema"] = None
132-
properties: Optional[Dict[str, "Schema"]] = None
133-
patternProperties: Optional[Dict[str, "Schema"]] = None
134-
additionalProperties: Optional["Schema"] = None
135-
propertyNames: Optional["Schema"] = None
136-
unevaluatedItems: Optional["Schema"] = None
137-
unevaluatedProperties: Optional["Schema"] = None
121+
allOf: Optional[List["SchemaOrBool"]] = None
122+
anyOf: Optional[List["SchemaOrBool"]] = None
123+
oneOf: Optional[List["SchemaOrBool"]] = None
124+
not_: Optional["SchemaOrBool"] = Field(default=None, alias="not")
125+
if_: Optional["SchemaOrBool"] = Field(default=None, alias="if")
126+
then: Optional["SchemaOrBool"] = None
127+
else_: Optional["SchemaOrBool"] = Field(default=None, alias="else")
128+
dependentSchemas: Optional[Dict[str, "SchemaOrBool"]] = None
129+
prefixItems: Optional[List["SchemaOrBool"]] = None
130+
# TODO: uncomment and remove below when deprecating Pydantic v1
131+
# It generales a list of schemas for tuples, before prefixItems was available
132+
# items: Optional["SchemaOrBool"] = None
133+
items: Optional[Union["SchemaOrBool", List["SchemaOrBool"]]] = None
134+
contains: Optional["SchemaOrBool"] = None
135+
properties: Optional[Dict[str, "SchemaOrBool"]] = None
136+
patternProperties: Optional[Dict[str, "SchemaOrBool"]] = None
137+
additionalProperties: Optional["SchemaOrBool"] = None
138+
propertyNames: Optional["SchemaOrBool"] = None
139+
unevaluatedItems: Optional["SchemaOrBool"] = None
140+
unevaluatedProperties: Optional["SchemaOrBool"] = None
138141
# Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-structural
139142
# A Vocabulary for Structural Validation
140143
type: Optional[str] = None
@@ -164,7 +167,7 @@ class Schema(BaseModel):
164167
# A Vocabulary for the Contents of String-Encoded Data
165168
contentEncoding: Optional[str] = None
166169
contentMediaType: Optional[str] = None
167-
contentSchema: Optional["Schema"] = None
170+
contentSchema: Optional["SchemaOrBool"] = None
168171
# Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-basic-meta
169172
# A Vocabulary for Basic Meta-Data Annotations
170173
title: Optional[str] = None
@@ -191,6 +194,11 @@ class Config:
191194
extra: str = "allow"
192195

193196

197+
# Ref: https://json-schema.org/draft/2020-12/json-schema-core.html#name-json-schema-documents
198+
# A JSON Schema MUST be an object or a boolean.
199+
SchemaOrBool = Union[Schema, bool]
200+
201+
194202
class Example(BaseModel):
195203
summary: Optional[str] = None
196204
description: Optional[str] = None
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
from typing import Union
2+
3+
from fastapi import FastAPI
4+
from fastapi.testclient import TestClient
5+
from pydantic import BaseModel
6+
7+
8+
class FooBaseModel(BaseModel):
9+
class Config:
10+
extra = "forbid"
11+
12+
13+
class Foo(FooBaseModel):
14+
pass
15+
16+
17+
app = FastAPI()
18+
19+
20+
@app.post("/")
21+
async def post(
22+
foo: Union[Foo, None] = None,
23+
):
24+
return foo
25+
26+
27+
client = TestClient(app)
28+
29+
30+
def test_call_invalid():
31+
response = client.post("/", json={"foo": {"bar": "baz"}})
32+
assert response.status_code == 422
33+
34+
35+
def test_call_valid():
36+
response = client.post("/", json={})
37+
assert response.status_code == 200
38+
assert response.json() == {}
39+
40+
41+
def test_openapi_schema():
42+
response = client.get("/openapi.json")
43+
assert response.status_code == 200, response.text
44+
assert response.json() == {
45+
"openapi": "3.1.0",
46+
"info": {"title": "FastAPI", "version": "0.1.0"},
47+
"paths": {
48+
"/": {
49+
"post": {
50+
"summary": "Post",
51+
"operationId": "post__post",
52+
"requestBody": {
53+
"content": {
54+
"application/json": {
55+
"schema": {"$ref": "#/components/schemas/Foo"}
56+
}
57+
}
58+
},
59+
"responses": {
60+
"200": {
61+
"description": "Successful Response",
62+
"content": {"application/json": {"schema": {}}},
63+
},
64+
"422": {
65+
"description": "Validation Error",
66+
"content": {
67+
"application/json": {
68+
"schema": {
69+
"$ref": "#/components/schemas/HTTPValidationError"
70+
}
71+
}
72+
},
73+
},
74+
},
75+
}
76+
}
77+
},
78+
"components": {
79+
"schemas": {
80+
"Foo": {
81+
"properties": {},
82+
"additionalProperties": False,
83+
"type": "object",
84+
"title": "Foo",
85+
},
86+
"HTTPValidationError": {
87+
"properties": {
88+
"detail": {
89+
"items": {"$ref": "#/components/schemas/ValidationError"},
90+
"type": "array",
91+
"title": "Detail",
92+
}
93+
},
94+
"type": "object",
95+
"title": "HTTPValidationError",
96+
},
97+
"ValidationError": {
98+
"properties": {
99+
"loc": {
100+
"items": {
101+
"anyOf": [{"type": "string"}, {"type": "integer"}]
102+
},
103+
"type": "array",
104+
"title": "Location",
105+
},
106+
"msg": {"type": "string", "title": "Message"},
107+
"type": {"type": "string", "title": "Error Type"},
108+
},
109+
"type": "object",
110+
"required": ["loc", "msg", "type"],
111+
"title": "ValidationError",
112+
},
113+
}
114+
},
115+
}

0 commit comments

Comments
 (0)