]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
🐛 Fix JSON Schema accepting bools as valid JSON Schemas, e.g. `additionalProperties...
authorSebastián Ramírez <tiangolo@gmail.com>
Sun, 2 Jul 2023 15:58:23 +0000 (17:58 +0200)
committerGitHub <noreply@github.com>
Sun, 2 Jul 2023 15:58:23 +0000 (17:58 +0200)
* 🐛 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

fastapi/openapi/models.py
tests/test_additional_properties_bool.py [new file with mode: 0644]

index 7420d3b55a0979a31ee07a8b67c4cf68bbfab04c..a2ea53607330100957e2f002e8dc9cd986e20c15 100644 (file)
@@ -114,27 +114,30 @@ class Schema(BaseModel):
     dynamicAnchor: Optional[str] = Field(default=None, alias="$dynamicAnchor")
     ref: Optional[str] = Field(default=None, alias="$ref")
     dynamicRef: Optional[str] = Field(default=None, alias="$dynamicRef")
-    defs: Optional[Dict[str, "Schema"]] = Field(default=None, alias="$defs")
+    defs: Optional[Dict[str, "SchemaOrBool"]] = Field(default=None, alias="$defs")
     comment: Optional[str] = Field(default=None, alias="$comment")
     # Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-a-vocabulary-for-applying-s
     # A Vocabulary for Applying Subschemas
-    allOf: Optional[List["Schema"]] = None
-    anyOf: Optional[List["Schema"]] = None
-    oneOf: Optional[List["Schema"]] = None
-    not_: Optional["Schema"] = Field(default=None, alias="not")
-    if_: Optional["Schema"] = Field(default=None, alias="if")
-    then: Optional["Schema"] = None
-    else_: Optional["Schema"] = Field(default=None, alias="else")
-    dependentSchemas: Optional[Dict[str, "Schema"]] = None
-    prefixItems: Optional[List["Schema"]] = None
-    items: Optional[Union["Schema", List["Schema"]]] = None
-    contains: Optional["Schema"] = None
-    properties: Optional[Dict[str, "Schema"]] = None
-    patternProperties: Optional[Dict[str, "Schema"]] = None
-    additionalProperties: Optional["Schema"] = None
-    propertyNames: Optional["Schema"] = None
-    unevaluatedItems: Optional["Schema"] = None
-    unevaluatedProperties: Optional["Schema"] = None
+    allOf: Optional[List["SchemaOrBool"]] = None
+    anyOf: Optional[List["SchemaOrBool"]] = None
+    oneOf: Optional[List["SchemaOrBool"]] = None
+    not_: Optional["SchemaOrBool"] = Field(default=None, alias="not")
+    if_: Optional["SchemaOrBool"] = Field(default=None, alias="if")
+    then: Optional["SchemaOrBool"] = None
+    else_: Optional["SchemaOrBool"] = Field(default=None, alias="else")
+    dependentSchemas: Optional[Dict[str, "SchemaOrBool"]] = None
+    prefixItems: Optional[List["SchemaOrBool"]] = None
+    # TODO: uncomment and remove below when deprecating Pydantic v1
+    # It generales a list of schemas for tuples, before prefixItems was available
+    # items: Optional["SchemaOrBool"] = None
+    items: Optional[Union["SchemaOrBool", List["SchemaOrBool"]]] = None
+    contains: Optional["SchemaOrBool"] = None
+    properties: Optional[Dict[str, "SchemaOrBool"]] = None
+    patternProperties: Optional[Dict[str, "SchemaOrBool"]] = None
+    additionalProperties: Optional["SchemaOrBool"] = None
+    propertyNames: Optional["SchemaOrBool"] = None
+    unevaluatedItems: Optional["SchemaOrBool"] = None
+    unevaluatedProperties: Optional["SchemaOrBool"] = None
     # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-structural
     # A Vocabulary for Structural Validation
     type: Optional[str] = None
@@ -164,7 +167,7 @@ class Schema(BaseModel):
     # A Vocabulary for the Contents of String-Encoded Data
     contentEncoding: Optional[str] = None
     contentMediaType: Optional[str] = None
-    contentSchema: Optional["Schema"] = None
+    contentSchema: Optional["SchemaOrBool"] = None
     # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-basic-meta
     # A Vocabulary for Basic Meta-Data Annotations
     title: Optional[str] = None
@@ -191,6 +194,11 @@ class Schema(BaseModel):
         extra: str = "allow"
 
 
+# Ref: https://json-schema.org/draft/2020-12/json-schema-core.html#name-json-schema-documents
+# A JSON Schema MUST be an object or a boolean.
+SchemaOrBool = Union[Schema, bool]
+
+
 class Example(BaseModel):
     summary: Optional[str] = None
     description: Optional[str] = None
diff --git a/tests/test_additional_properties_bool.py b/tests/test_additional_properties_bool.py
new file mode 100644 (file)
index 0000000..e35c263
--- /dev/null
@@ -0,0 +1,115 @@
+from typing import Union
+
+from fastapi import FastAPI
+from fastapi.testclient import TestClient
+from pydantic import BaseModel
+
+
+class FooBaseModel(BaseModel):
+    class Config:
+        extra = "forbid"
+
+
+class Foo(FooBaseModel):
+    pass
+
+
+app = FastAPI()
+
+
+@app.post("/")
+async def post(
+    foo: Union[Foo, None] = None,
+):
+    return foo
+
+
+client = TestClient(app)
+
+
+def test_call_invalid():
+    response = client.post("/", json={"foo": {"bar": "baz"}})
+    assert response.status_code == 422
+
+
+def test_call_valid():
+    response = client.post("/", json={})
+    assert response.status_code == 200
+    assert response.json() == {}
+
+
+def test_openapi_schema():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/": {
+                "post": {
+                    "summary": "Post",
+                    "operationId": "post__post",
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {"$ref": "#/components/schemas/Foo"}
+                            }
+                        }
+                    },
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "Foo": {
+                    "properties": {},
+                    "additionalProperties": False,
+                    "type": "object",
+                    "title": "Foo",
+                },
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                            "type": "array",
+                            "title": "Detail",
+                        }
+                    },
+                    "type": "object",
+                    "title": "HTTPValidationError",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                            "type": "array",
+                            "title": "Location",
+                        },
+                        "msg": {"type": "string", "title": "Message"},
+                        "type": {"type": "string", "title": "Error Type"},
+                    },
+                    "type": "object",
+                    "required": ["loc", "msg", "type"],
+                    "title": "ValidationError",
+                },
+            }
+        },
+    }