From 07e1dea467c9654ea771bfef23cb3bf9654feb01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 2 Jul 2023 17:58:23 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fix=20JSON=20Schema=20accepting?= =?UTF-8?q?=20bools=20as=20valid=20JSON=20Schemas,=20e.g.=20`additionalPro?= =?UTF-8?q?perties:=20false`=20(#9781)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 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 | 46 +++++---- tests/test_additional_properties_bool.py | 115 +++++++++++++++++++++++ 2 files changed, 142 insertions(+), 19 deletions(-) create mode 100644 tests/test_additional_properties_bool.py diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index 7420d3b55..a2ea53607 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -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 index 000000000..e35c26342 --- /dev/null +++ b/tests/test_additional_properties_bool.py @@ -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", + }, + } + }, + }