From 9e7c55f99bd01447e63c7ede134b1a774223f3af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Nov 2018 13:24:06 +0400 Subject: [PATCH 01/65] Update schema tests to conform to JSON Schema spec --- tests/test_schema.py | 123 ++++++++++++++++++++++++++++--------------- 1 file changed, 82 insertions(+), 41 deletions(-) diff --git a/tests/test_schema.py b/tests/test_schema.py index f2241197c3..d000650467 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -20,9 +20,10 @@ class ApplePie(BaseModel): s = { 'type': 'object', 'properties': { - 'a': {'type': 'float', 'required': True, 'title': 'A'}, - 'b': {'type': 'int', 'required': False, 'title': 'B', 'default': 10}, + 'a': {'type': 'number', 'title': 'A'}, + 'b': {'type': 'integer', 'title': 'B', 'default': 10}, }, + 'required': ['a'], 'title': 'ApplePie', 'description': 'This is a test.', } @@ -47,13 +48,16 @@ class Config: 'type': 'object', 'title': 'Apple Pie', 'properties': { - 'Snap': {'type': 'float', 'required': True, 'title': 'Snap'}, - 'Crackle': {'type': 'int', 'required': False, 'title': 'Crackle', 'default': 10}, + 'Snap': {'type': 'number', 'title': 'Snap'}, + 'Crackle': {'type': 'integer', 'title': 'Crackle', 'default': 10}, }, + 'required': ['Snap'], } assert ApplePie.schema() == s - assert ApplePie.schema() == s - assert list(ApplePie.schema(by_alias=True)['properties'].keys()) == ['Snap', 'Crackle'] + assert list(ApplePie.schema(by_alias=True)['properties'].keys()) == [ + 'Snap', + 'Crackle', + ] assert list(ApplePie.schema(by_alias=False)['properties'].keys()) == ['a', 'b'] @@ -71,14 +75,16 @@ class Bar(BaseModel): 'type': 'object', 'title': 'Bar', 'properties': { - 'a': {'type': 'int', 'title': 'A', 'required': True}, + 'a': {'type': 'integer', 'title': 'A'}, 'b': { 'type': 'object', - 'title': 'B', - 'properties': {'b': {'type': 'float', 'title': 'B', 'required': True}}, - 'required': False, + 'title': 'Foo', + 'description': 'hello', + 'properties': {'b': {'type': 'number', 'title': 'B'}}, + 'required': ['b'], }, }, + 'required': ['a'], } @@ -97,9 +103,14 @@ class Model(BaseModel): 'type': 'object', 'title': 'Model', 'properties': { - 'foo': {'type': 'int', 'title': 'Foo is Great', 'required': False, 'default': 4}, - 'bar': {'type': 'str', 'title': 'Bar', 'required': True, 'description': 'this description of bar'}, + 'foo': {'type': 'integer', 'title': 'Foo is Great', 'default': 4}, + 'bar': { + 'type': 'string', + 'title': 'Bar', + 'description': 'this description of bar', + }, }, + 'required': ['bar'], } @@ -122,16 +133,17 @@ class SpamEnum(str, Enum): class Model(BaseModel): foo: FooEnum bar: BarEnum - spam: SpamEnum = Schema(None, choice_names={'f': 'Sausage'}) + spam: SpamEnum = Schema(None) assert Model.schema() == { 'type': 'object', 'title': 'Model', 'properties': { - 'foo': {'type': 'enum', 'title': 'Foo', 'required': True, 'choices': [('f', 'Foo'), ('b', 'Bar')]}, - 'bar': {'type': 'int', 'title': 'Bar', 'required': True, 'choices': [(1, 'Foo'), (2, 'Bar')]}, - 'spam': {'type': 'str', 'title': 'Spam', 'required': False, 'choices': [('f', 'Sausage'), ('b', 'Bar')]}, + 'foo': {'title': 'Foo', 'enum': ['f', 'b']}, + 'bar': {'type': 'integer', 'title': 'Bar', 'enum': [1, 2]}, + 'spam': {'type': 'string', 'title': 'Spam', 'enum': ['f', 'b']}, }, + 'required': ['foo', 'bar'], } @@ -150,15 +162,14 @@ class Model(BaseModel): ' "properties": {\n' ' "a": {\n' ' "title": "A",\n' - ' "required": false,\n' ' "default": "foobar",\n' - ' "type": "bytes"\n' + ' "type": "string",\n' + ' "format": "binary"\n' ' },\n' ' "b": {\n' ' "title": "B",\n' - ' "required": false,\n' ' "default": 12.34,\n' - ' "type": "Decimal"\n' + ' "type": "number"\n' ' }\n' ' }\n' '}' @@ -177,12 +188,17 @@ class Bar(BaseModel): 'type': 'object', 'properties': { 'b': { - 'type': 'list', - 'item_type': {'type': 'object', 'properties': {'a': {'type': 'float', 'title': 'A', 'required': True}}}, + 'type': 'array', + 'items': { + 'title': 'Foo', + 'type': 'object', + 'properties': {'a': {'type': 'number', 'title': 'A'}}, + 'required': ['a'], + }, 'title': 'B', - 'required': True, } }, + 'required': ['b'], } @@ -193,7 +209,7 @@ class Model(BaseModel): assert Model.schema() == { 'title': 'Model', 'type': 'object', - 'properties': {'a': {'type': 'str', 'title': 'A', 'required': False}}, + 'properties': {'a': {'type': 'string', 'title': 'A'}}, } @@ -204,7 +220,8 @@ class Model(BaseModel): assert Model.schema() == { 'title': 'Model', 'type': 'object', - 'properties': {'a': {'type': 'any', 'title': 'A', 'required': True}}, + 'properties': {'a': {'title': 'A'}}, + 'required': ['a'], } @@ -215,7 +232,15 @@ class Model(BaseModel): assert Model.schema() == { 'title': 'Model', 'type': 'object', - 'properties': {'a': {'title': 'A', 'required': True, 'type': 'set', 'item_type': 'int'}}, + 'properties': { + 'a': { + 'title': 'A', + 'type': 'array', + 'uniqueItems': True, + 'items': {'type': 'integer'}, + } + }, + 'required': ['a'], } @@ -229,11 +254,22 @@ class Model(BaseModel): 'properties': { 'a': { 'title': 'A', - 'required': True, - 'type': 'tuple', - 'item_types': ['str', 'int', {'type': 'any_of', 'types': ['str', 'int', 'float']}, 'float'], + 'type': 'array', + 'items': [ + {'type': 'string'}, + {'type': 'integer'}, + { + 'anyOf': [ + {'type': 'string'}, + {'type': 'integer'}, + {'type': 'number'}, + ] + }, + {'type': 'number'}, + ], } }, + 'required': ['a'], } @@ -246,30 +282,35 @@ class Model(BaseModel): a: Union[int, str] b: List[int] - c: Dict[int, Foo] + c: Dict[str, Foo] d: Union[None, Foo] e: Dict[str, Any] - assert Model.schema() == { + model_schema = Model.schema() + assert model_schema == { 'title': 'Model', 'description': 'party time', 'type': 'object', 'properties': { - 'a': {'title': 'A', 'required': True, 'type': 'any_of', 'types': ['int', 'str']}, - 'b': {'title': 'B', 'required': True, 'type': 'list', 'item_type': 'int'}, + 'a': {'title': 'A', 'anyOf': [{'type': 'integer'}, {'type': 'string'}]}, + 'b': {'title': 'B', 'type': 'array', 'items': {'type': 'integer'}}, 'c': { 'title': 'C', - 'required': True, - 'type': 'mapping', - 'item_type': {'type': 'object', 'properties': {'a': {'title': 'A', 'required': True, 'type': 'float'}}}, - 'key_type': 'int', + 'type': 'object', + 'additionalProperties': { + 'title': 'Foo', + 'type': 'object', + 'properties': {'a': {'title': 'A', 'type': 'number'}}, + 'required': ['a'], + }, }, 'd': { - 'title': 'D', - 'required': False, + 'title': 'Foo', 'type': 'object', - 'properties': {'a': {'title': 'A', 'required': True, 'type': 'float'}}, + 'properties': {'a': {'title': 'A', 'type': 'number'}}, + 'required': ['a'], }, - 'e': {'title': 'E', 'required': True, 'type': 'mapping', 'item_type': 'any', 'key_type': 'str'}, + 'e': {'title': 'E', 'type': 'object'}, }, + 'required': ['a', 'b', 'c', 'e'], } From e3196f690a11ee0d43aa63fa078661608bd956e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Nov 2018 13:25:21 +0400 Subject: [PATCH 02/65] Add JSON Schema tests for all supported types including datetime and all supported Pydantic.types --- tests/test_schema.py | 234 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) diff --git a/tests/test_schema.py b/tests/test_schema.py index d000650467..120186dd0e 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -1,11 +1,17 @@ import json +from datetime import date, datetime, time, timedelta from decimal import Decimal from enum import Enum, IntEnum from typing import Any, Dict, List, Optional, Set, Tuple, Union +from uuid import UUID import pytest from pydantic import BaseModel, Schema, ValidationError +from pydantic.types import (DSN, UUID1, UUID3, UUID4, UUID5, ConstrainedDecimal, ConstrainedFloat, ConstrainedInt, + ConstrainedStr, DirectoryPath, EmailStr, FilePath, Json, NameEmail, NegativeFloat, + NegativeInt, NoneBytes, NoneStr, NoneStrBytes, PositiveFloat, PositiveInt, StrBytes, + StrictStr, UrlStr, condecimal, confloat, conint, constr, urlstr) def test_key(): @@ -314,3 +320,231 @@ class Model(BaseModel): }, 'required': ['a', 'b', 'c', 'e'], } + + +def test_date_types(): + class Model(BaseModel): + a: datetime + b: date + c: time + d: timedelta + + model_schema = Model.schema() + assert model_schema == { + 'title': 'Model', + 'type': 'object', + 'properties': { + 'a': {'title': 'A', 'type': 'string', 'format': 'date-time'}, + 'b': {'title': 'B', 'type': 'string', 'format': 'date'}, + 'c': {'title': 'C', 'type': 'string', 'format': 'time'}, + 'd': {'title': 'D', 'type': 'string', 'format': 'time-delta'}, + }, + 'required': ['a', 'b', 'c', 'd'], + } + + +def test_str_basic_types(): + class Model(BaseModel): + a: NoneStr + b: NoneBytes + c: StrBytes + d: NoneStrBytes + + model_schema = Model.schema() + assert model_schema == { + 'title': 'Model', + 'type': 'object', + 'properties': { + 'a': {'title': 'A', 'type': 'string'}, + 'b': {'title': 'B', 'type': 'string', 'format': 'binary'}, + 'c': { + 'title': 'C', + 'anyOf': [{'type': 'string'}, {'type': 'string', 'format': 'binary'}], + }, + 'd': { + 'title': 'D', + 'anyOf': [{'type': 'string'}, {'type': 'string', 'format': 'binary'}], + }, + }, + 'required': ['c'], + } + + +def test_str_constrained_types(): + class Model(BaseModel): + a: StrictStr + b: ConstrainedStr + c: constr(min_length=3, max_length=5, regex='^text$') + + model_schema = Model.schema() + assert model_schema == { + 'title': 'Model', + 'type': 'object', + 'properties': { + 'a': {'title': 'A', 'type': 'string'}, + 'b': {'title': 'B', 'type': 'string'}, + 'c': { + 'title': 'C', + 'type': 'string', + 'minLength': 3, + 'maxLength': 5, + 'pattern': '^text$', + }, + }, + 'required': ['a', 'b', 'c'], + } + + +def test_special_str_types(): + class Model(BaseModel): + a: EmailStr + b: UrlStr + c: urlstr(min_length=5, max_length=10) + d: NameEmail + e: DSN + + model_schema = Model.schema() + assert model_schema == { + 'title': 'Model', + 'type': 'object', + 'properties': { + 'a': {'title': 'A', 'type': 'string', 'format': 'email'}, + 'b': { + 'title': 'B', + 'type': 'string', + 'format': 'uri', + 'minLength': 1, + 'maxLength': 2 ** 16, + }, + 'c': { + 'title': 'C', + 'type': 'string', + 'format': 'uri', + 'minLength': 5, + 'maxLength': 10, + }, + 'd': {'title': 'D', 'type': 'string', 'format': 'name-email'}, + 'e': {'title': 'E', 'type': 'string', 'format': 'dsn'}, + }, + 'required': ['a', 'b', 'c', 'd', 'e'], + } + + +def test_special_int_types(): + class Model(BaseModel): + a: ConstrainedInt + b: conint(gt=5, lt=10) + c: conint(ge=5, le=10) + d: PositiveInt + e: NegativeInt + + model_schema = Model.schema() + assert model_schema == { + 'title': 'Model', + 'type': 'object', + 'properties': { + 'a': {'title': 'A', 'type': 'integer'}, + 'b': { + 'title': 'B', + 'type': 'integer', + 'exclusiveMinimum': 5, + 'exclusiveMaximum': 10, + }, + 'c': {'title': 'C', 'type': 'integer', 'minimum': 5, 'maximum': 10}, + 'd': {'title': 'D', 'type': 'integer', 'exclusiveMinimum': 0}, + 'e': {'title': 'E', 'type': 'integer', 'exclusiveMaximum': 0}, + }, + 'required': ['a', 'b', 'c', 'd', 'e'], + } + + +def test_special_float_types(): + class Model(BaseModel): + a: ConstrainedFloat + b: confloat(gt=5, lt=10) + c: confloat(ge=5, le=10) + d: PositiveFloat + e: NegativeFloat + f: ConstrainedDecimal + g: condecimal(gt=5, lt=10) + h: condecimal(ge=5, le=10) + + model_schema = Model.schema() + assert model_schema == { + 'title': 'Model', + 'type': 'object', + 'properties': { + 'a': {'title': 'A', 'type': 'number'}, + 'b': { + 'title': 'B', + 'type': 'number', + 'exclusiveMinimum': 5, + 'exclusiveMaximum': 10, + }, + 'c': {'title': 'C', 'type': 'number', 'minimum': 5, 'maximum': 10}, + 'd': {'title': 'D', 'type': 'number', 'exclusiveMinimum': 0}, + 'e': {'title': 'E', 'type': 'number', 'exclusiveMaximum': 0}, + 'f': {'title': 'F', 'type': 'number'}, + 'g': { + 'title': 'G', + 'type': 'number', + 'exclusiveMinimum': 5, + 'exclusiveMaximum': 10, + }, + 'h': {'title': 'H', 'type': 'number', 'minimum': 5, 'maximum': 10}, + }, + 'required': ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], + } + + +def test_uuid_types(): + class Model(BaseModel): + a: UUID + b: UUID1 + c: UUID3 + d: UUID4 + e: UUID5 + + model_schema = Model.schema() + assert model_schema == { + 'title': 'Model', + 'type': 'object', + 'properties': { + 'a': {'title': 'A', 'type': 'string', 'format': 'uuid'}, + 'b': {'title': 'B', 'type': 'string', 'format': 'uuid1'}, + 'c': {'title': 'C', 'type': 'string', 'format': 'uuid3'}, + 'd': {'title': 'D', 'type': 'string', 'format': 'uuid4'}, + 'e': {'title': 'E', 'type': 'string', 'format': 'uuid5'}, + }, + 'required': ['a', 'b', 'c', 'd', 'e'], + } + + +def test_path_types(): + class Model(BaseModel): + a: FilePath + b: DirectoryPath + + model_schema = Model.schema() + assert model_schema == { + 'title': 'Model', + 'type': 'object', + 'properties': { + 'a': {'title': 'A', 'type': 'string', 'format': 'file-path'}, + 'b': {'title': 'B', 'type': 'string', 'format': 'directory-path'}, + }, + 'required': ['a', 'b'], + } + + +def test_json_type(): + class Model(BaseModel): + a: Json + + model_schema = Model.schema() + assert model_schema == { + 'title': 'Model', + 'type': 'object', + 'properties': {'a': {'title': 'A', 'type': 'string', 'format': 'json-string'}}, + 'required': ['a'], + } From f905f830fde1204ef151f4ef4a635fb45960976d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Nov 2018 13:33:41 +0400 Subject: [PATCH 03/65] Add JSON Schema conforming schema sub module --- pydantic/schema.py | 186 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 pydantic/schema.py diff --git a/pydantic/schema.py b/pydantic/schema.py new file mode 100644 index 0000000000..c37fc48d15 --- /dev/null +++ b/pydantic/schema.py @@ -0,0 +1,186 @@ +from datetime import date, datetime, time, timedelta +from decimal import Decimal +from enum import Enum +from pathlib import Path +from typing import Any, Dict +from uuid import UUID + +from . import main +from .fields import Field, Shape +from .types import DSN, UUID1, UUID3, UUID4, UUID5, DirectoryPath, EmailStr, FilePath, Json, NameEmail, UrlStr +from .utils import clean_docstring + + +def field_schema(field: Field, by_alias=True): + s = dict(title=field._schema.title or field.alias.title()) + + if not field.required and field.default is not None: + s['default'] = field.default + s.update(field._schema.extra) + + ts = field_type_schema(field, by_alias) + s.update(ts) + return s + + +def field_type_schema(field: Field, by_alias: bool): + if field.shape is Shape.LIST: + return {'type': 'array', 'items': field_singleton_schema(field, by_alias)} + elif field.shape is Shape.SET: + return { + 'type': 'array', + 'uniqueItems': True, + 'items': field_singleton_schema(field, by_alias), + } + elif field.shape is Shape.MAPPING: + sub_field_schema = field_singleton_schema(field, by_alias) + if sub_field_schema: + # The dict values are not simply Any + return { + 'type': 'object', + 'additionalProperties': sub_field_schema, + } + else: + # The dict values are Any, no need to declare it + return { + 'type': 'object' + } + elif field.shape is Shape.TUPLE: + sub_schema = [field_type_schema(sf, by_alias) for sf in field.sub_fields] + if len(sub_schema) == 1: + sub_schema = sub_schema[0] + return { + 'type': 'array', + 'items': sub_schema, + } + else: + assert field.shape is Shape.SINGLETON, field.shape + return field_singleton_schema(field, by_alias) + + +def model_schema(class_: 'main.BaseModel', by_alias=True) -> Dict[str, Any]: + s = {'title': class_.__config__.title or class_.__name__} + if class_.__doc__: + s['description'] = clean_docstring(class_.__doc__) + + s.update(model_type_schema(class_, by_alias)) + return s + + +def model_type_schema(class_: 'main.BaseModel', by_alias: bool): + if by_alias: + properties = {f.alias: field_schema(f, by_alias) for f in class_.__fields__.values()} + required = [f.alias for f in class_.__fields__.values() if f.required] + else: + properties = {k: field_schema(f, by_alias) for k, f in class_.__fields__.items()} + required = [k for k, f in class_.__fields__.items() if f.required] + out_schema = {'type': 'object', 'properties': properties} + if required: + out_schema['required'] = required + return out_schema + + +def field_singleton_schema(field: Field, by_alias: bool): + if field.sub_fields: + if len(field.sub_fields) == 1: + return field_type_schema(field.sub_fields[0], by_alias) + else: + return {'anyOf': [field_type_schema(sf, by_alias) for sf in field.sub_fields]} + if field.type_ is Any: + return {} # no restrictions + schema_value = {} + if issubclass(field.type_, Enum): + schema_value.update({'enum': [item.value for item in field.type_]}) + # Don't return immediately, to allow adding specific types + # For constrained strings + if hasattr(field.type_, 'min_length'): + if field.type_.min_length is not None: + schema_value.update({'minLength': field.type_.min_length}) + if hasattr(field.type_, 'max_length'): + if field.type_.max_length is not None: + schema_value.update({'maxLength': field.type_.max_length}) + if hasattr(field.type_, 'regex'): + if field.type_.regex is not None: + schema_value.update({'pattern': field.type_.regex.pattern}) + # For constrained numbers + if hasattr(field.type_, 'gt'): + if field.type_.gt is not None: + schema_value.update({'exclusiveMinimum': field.type_.gt}) + if hasattr(field.type_, 'lt'): + if field.type_.lt is not None: + schema_value.update({'exclusiveMaximum': field.type_.lt}) + if hasattr(field.type_, 'ge'): + if field.type_.ge is not None: + schema_value.update({'minimum': field.type_.ge}) + if hasattr(field.type_, 'le'): + if field.type_.le is not None: + schema_value.update({'maximum': field.type_.le}) + # Sub-classes of str must go before str + if issubclass(field.type_, EmailStr): + schema_value.update({'type': 'string', 'format': 'email'}) + return schema_value + if issubclass(field.type_, UrlStr): + schema_value.update({'type': 'string', 'format': 'uri'}) + return schema_value + if issubclass(field.type_, DSN): + schema_value.update({'type': 'string', 'format': 'dsn'}) + return schema_value + if issubclass(field.type_, str): + schema_value.update({'type': 'string'}) + return schema_value + elif issubclass(field.type_, bytes): + schema_value.update({'type': 'string', 'format': 'binary'}) + return schema_value + elif issubclass(field.type_, bool): + schema_value.update({'type': 'boolean'}) + return schema_value + elif issubclass(field.type_, int): + schema_value.update({'type': 'integer'}) + return schema_value + elif issubclass(field.type_, float): + schema_value.update({'type': 'number'}) + return schema_value + elif issubclass(field.type_, Decimal): + schema_value.update({'type': 'number'}) + return schema_value + elif issubclass(field.type_, UUID1): + schema_value.update({'type': 'string', 'format': 'uuid1'}) + return schema_value + elif issubclass(field.type_, UUID3): + schema_value.update({'type': 'string', 'format': 'uuid3'}) + return schema_value + elif issubclass(field.type_, UUID4): + schema_value.update({'type': 'string', 'format': 'uuid4'}) + return schema_value + elif issubclass(field.type_, UUID5): + schema_value.update({'type': 'string', 'format': 'uuid5'}) + return schema_value + elif issubclass(field.type_, UUID): + schema_value.update({'type': 'string', 'format': 'uuid'}) + return schema_value + elif issubclass(field.type_, NameEmail): + schema_value.update({'type': 'string', 'format': 'name-email'}) + return schema_value + # This is the last value that can also be an Enum + if schema_value: + return schema_value + # Path subclasses must go before Path + elif issubclass(field.type_, FilePath): + return {'type': 'string', 'format': 'file-path'} + elif issubclass(field.type_, DirectoryPath): + return {'type': 'string', 'format': 'directory-path'} + elif issubclass(field.type_, Path): + return {'type': 'string', 'format': 'path'} + elif issubclass(field.type_, datetime): + return {'type': 'string', 'format': 'date-time'} + elif issubclass(field.type_, date): + return {'type': 'string', 'format': 'date'} + elif issubclass(field.type_, time): + return {'type': 'string', 'format': 'time'} + elif issubclass(field.type_, timedelta): + return {'type': 'string', 'format': 'time-delta'} + elif issubclass(field.type_, Json): + return {'type': 'string', 'format': 'json-string'} + elif issubclass(field.type_, main.BaseModel): + return model_schema(field.type_, by_alias) + raise ValueError('Value not declarable with JSON Schema') From a702d6de2c91005de697bda9f598117e754e5c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Nov 2018 13:37:48 +0400 Subject: [PATCH 04/65] Update BaseModel to use schema module for JSON Schema generation and update/simplify internal Schema methods --- pydantic/main.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/pydantic/main.py b/pydantic/main.py index b9636925a7..16104f3622 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -17,6 +17,8 @@ from .utils import clean_docstring, truncate, validate_field_name from .validators import dict_validator +from .schema import model_schema, model_type_schema + class BaseConfig: title = None @@ -164,7 +166,7 @@ def __new__(mcs, name, bases, namespace): class BaseModel(metaclass=MetaModel): # populated by the metaclass, defined here to help IDEs only - __fields__ = {} + __fields__: Dict[str, Field] = {} __validators__ = {} Config = BaseConfig @@ -318,25 +320,14 @@ def fields(self): @classmethod def type_schema(cls, by_alias): - return { - 'type': 'object', - 'properties': ( - {f.alias: f.schema(by_alias) for f in cls.__fields__.values()} - if by_alias - else {k: f.schema(by_alias) for k, f in cls.__fields__.items()} - ), - } + return model_type_schema(cls, by_alias=by_alias) @classmethod def schema(cls, by_alias=True) -> Dict[str, Any]: cached = cls._schema_cache.get(by_alias) if cached is not None: return cached - s = {'title': cls.__config__.title or cls.__name__} - if cls.__doc__: - s['description'] = clean_docstring(cls.__doc__) - - s.update(cls.type_schema(by_alias)) + s = model_schema(cls, by_alias=by_alias) cls._schema_cache[by_alias] = s return s From bc7342751bb00707f3bce8679de084349837c01a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Nov 2018 13:39:07 +0400 Subject: [PATCH 05/65] Remove Schema code from Field class, replaced with JSON Schema module --- pydantic/fields.py | 53 +--------------------------------------------- 1 file changed, 1 insertion(+), 52 deletions(-) diff --git a/pydantic/fields.py b/pydantic/fields.py index 249a8bd3b0..34c06e23dc 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -154,58 +154,7 @@ def prepare(self): self._populate_sub_fields() self._populate_validators() - def schema(self, by_alias=True): - s = dict(title=self._schema.title or self.alias.title(), required=self.required) - - if not self.required and self.default is not None: - s['default'] = self.default - s.update(self._schema.extra) - - ts = self.type_schema(by_alias) - s.update(ts if isinstance(ts, dict) else {'type': ts}) - return s - - def type_schema(self, by_alias): - if self.shape is Shape.LIST: - return {'type': 'list', 'item_type': self._singleton_schema(by_alias)} - if self.shape is Shape.SET: - return {'type': 'set', 'item_type': self._singleton_schema(by_alias)} - elif self.shape is Shape.MAPPING: - return { - 'type': 'mapping', - 'item_type': self._singleton_schema(by_alias), - 'key_type': self.key_field.type_schema(by_alias), - } - elif self.shape is Shape.TUPLE: - return {'type': 'tuple', 'item_types': [sf.type_schema(by_alias) for sf in self.sub_fields]} - else: - assert self.shape is Shape.SINGLETON, self.shape - return self._singleton_schema(by_alias) - - def _singleton_schema(self, by_alias): - if self.sub_fields: - if len(self.sub_fields) == 1: - return self.sub_fields[0].type_schema(by_alias) - else: - return {'type': 'any_of', 'types': [sf.type_schema(by_alias) for sf in self.sub_fields]} - elif self.type_ is Any: - return 'any' - elif issubclass(self.type_, Enum): - choice_names = self._schema.choice_names or {} - return { - 'type': display_as_type(self.type_), - 'choices': [ - (v.value, choice_names.get(v.value) or k.title()) for k, v in self.type_.__members__.items() - ], - } - - type_schema_method = getattr(self.type_, 'type_schema', None) - if callable(type_schema_method): - return type_schema_method(by_alias) - else: - return display_as_type(self.type_) - - def _populate_sub_fields(self): # noqa: C901 (ignore complexity) + def _populate_sub_fields(self): # typing interface is horrible, we have to do some ugly checks if isinstance(self.type_, type) and issubclass(self.type_, JsonWrapper): self.type_ = self.type_.inner_type From 1312ca4f8c1c3c48480911a4082141e7e4a73c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Nov 2018 20:13:18 +0400 Subject: [PATCH 06/65] Add submodules to test model name generation for JSON Schemas --- tests/schema_test_package/__init__.py | 0 tests/schema_test_package/modulea/__init__.py | 0 tests/schema_test_package/modulea/modela.py | 5 +++++ tests/schema_test_package/moduleb/__init__.py | 0 tests/schema_test_package/moduleb/modelb.py | 5 +++++ tests/schema_test_package/modulec/__init__.py | 0 tests/schema_test_package/modulec/modelc.py | 1 + 7 files changed, 11 insertions(+) create mode 100644 tests/schema_test_package/__init__.py create mode 100644 tests/schema_test_package/modulea/__init__.py create mode 100644 tests/schema_test_package/modulea/modela.py create mode 100644 tests/schema_test_package/moduleb/__init__.py create mode 100644 tests/schema_test_package/moduleb/modelb.py create mode 100644 tests/schema_test_package/modulec/__init__.py create mode 100644 tests/schema_test_package/modulec/modelc.py diff --git a/tests/schema_test_package/__init__.py b/tests/schema_test_package/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/schema_test_package/modulea/__init__.py b/tests/schema_test_package/modulea/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/schema_test_package/modulea/modela.py b/tests/schema_test_package/modulea/modela.py new file mode 100644 index 0000000000..75ac72d84e --- /dev/null +++ b/tests/schema_test_package/modulea/modela.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class Model(BaseModel): + a: str diff --git a/tests/schema_test_package/moduleb/__init__.py b/tests/schema_test_package/moduleb/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/schema_test_package/moduleb/modelb.py b/tests/schema_test_package/moduleb/modelb.py new file mode 100644 index 0000000000..81bcbfbde8 --- /dev/null +++ b/tests/schema_test_package/moduleb/modelb.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class Model(BaseModel): + a: int diff --git a/tests/schema_test_package/modulec/__init__.py b/tests/schema_test_package/modulec/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/schema_test_package/modulec/modelc.py b/tests/schema_test_package/modulec/modelc.py new file mode 100644 index 0000000000..5085fb08f0 --- /dev/null +++ b/tests/schema_test_package/modulec/modelc.py @@ -0,0 +1 @@ +from ..moduleb.modelb import Model From 18a2b253d03c6a50f7c7eca6b0fec9a55614bcde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Nov 2018 20:17:20 +0400 Subject: [PATCH 07/65] Refactor/rewrite schema module to generate definions and refs --- pydantic/schema.py | 373 +++++++++++++++++++++++++++++++++------------ 1 file changed, 275 insertions(+), 98 deletions(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index c37fc48d15..4d36d069e7 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -2,185 +2,362 @@ from decimal import Decimal from enum import Enum from pathlib import Path -from typing import Any, Dict +from typing import Any, Dict, Mapping, Sequence, Set, Tuple, Type from uuid import UUID from . import main from .fields import Field, Shape +from .json import pydantic_encoder from .types import DSN, UUID1, UUID3, UUID4, UUID5, DirectoryPath, EmailStr, FilePath, Json, NameEmail, UrlStr from .utils import clean_docstring -def field_schema(field: Field, by_alias=True): +def get_flat_models_from_model( + model: Type['main.BaseModel'] +) -> Set[Type['main.BaseModel']]: + flat_models: Set[Type[main.BaseModel]] = set() + assert issubclass(model, main.BaseModel) + flat_models.add(model) + for field in model.__fields__.values(): + flat_models = flat_models | get_flat_models_from_field(field) + return flat_models + + +def get_flat_models_from_field(field: Field) -> Set[Type['main.BaseModel']]: + flat_models: Set[Type[main.BaseModel]] = set() + if field.sub_fields: + flat_models = flat_models | get_flat_models_from_sub_fields(field.sub_fields) + elif issubclass(field.type_, main.BaseModel): + flat_models = flat_models | get_flat_models_from_model(field.type_) + return flat_models + + +def get_flat_models_from_sub_fields(fields) -> Set[Type['main.BaseModel']]: + flat_models: Set[Type[main.BaseModel]] = set() + for field in fields: + flat_models = flat_models | get_flat_models_from_field(field) + return flat_models + + +def get_flat_models_from_models( + models: Sequence[Type['main.BaseModel']] +) -> Set[Type['main.BaseModel']]: + flat_models: Set[Type[main.BaseModel]] = set() + for model in models: + flat_models = flat_models | get_flat_models_from_model(model) + return flat_models + + +def get_long_model_name(model: Type['main.BaseModel']): + assert issubclass(model, main.BaseModel) + prefix = model.__module__.replace('.', '__') + name = model.__name__ + return f'{prefix}__{name}' + + +def get_model_name_maps( + unique_models: Set[Type['main.BaseModel']] +) -> Tuple[Dict[str, Type['main.BaseModel']], Dict[Type['main.BaseModel'], str]]: + name_model_map: Dict[str, Type[main.BaseModel]] = {} + conflicting_names: Set[str] = set() + for model in unique_models: + model_name = model.__name__ + if model_name in conflicting_names: + model_name = get_long_model_name(model) + name_model_map[model_name] = model + elif model_name in name_model_map: + conflicting_names.add(model_name) + conflicting_model = name_model_map[model_name] + del name_model_map[model_name] + conflicting_model_name = get_long_model_name(conflicting_model) + name_model_map[conflicting_model_name] = conflicting_model + model_name = get_long_model_name(model) + name_model_map[model_name] = model + else: + name_model_map[model_name] = model + model_name_map = {v: k for k, v in name_model_map.items()} + return name_model_map, model_name_map + + +def field_schema( + field: Field, *, by_alias=True, model_name_map: Dict[Type['main.BaseModel'], str] +) -> Tuple[Dict[str, Any], Dict[str, Any]]: + schema_overrides = False s = dict(title=field._schema.title or field.alias.title()) + if field._schema.title: + schema_overrides = True if not field.required and field.default is not None: - s['default'] = field.default - s.update(field._schema.extra) + schema_overrides = True + if isinstance(field.default, (int, float, bool, str)): + s['default'] = field.default + else: + s['default'] = pydantic_encoder(field.default) + if field._schema.extra: + s.update(field._schema.extra) + schema_overrides = True - ts = field_type_schema(field, by_alias) - s.update(ts) - return s + f_schema, f_definitions = field_type_schema( + field, + by_alias=by_alias, + model_name_map=model_name_map, + schema_overrides=schema_overrides, + ) + # $ref will only be returned when there are no schema_overrides + if '$ref' in f_schema: + return f_schema, f_definitions + else: + s.update(f_schema) + return s, f_definitions -def field_type_schema(field: Field, by_alias: bool): +def field_type_schema( + field: Field, + *, + by_alias: bool, + model_name_map: Dict[Type['main.BaseModel'], str], + schema_overrides=False, +) -> Tuple[Dict[str, Any], Dict[str, Any]]: + definitions = {} if field.shape is Shape.LIST: - return {'type': 'array', 'items': field_singleton_schema(field, by_alias)} + f_schema, f_definitions = field_singleton_schema( + field, by_alias=by_alias, model_name_map=model_name_map + ) + definitions.update(f_definitions) + return {'type': 'array', 'items': f_schema}, definitions elif field.shape is Shape.SET: - return { - 'type': 'array', - 'uniqueItems': True, - 'items': field_singleton_schema(field, by_alias), - } + f_schema, f_definitions = field_singleton_schema( + field, by_alias=by_alias, model_name_map=model_name_map + ) + definitions.update(f_definitions) + return {'type': 'array', 'uniqueItems': True, 'items': f_schema}, definitions elif field.shape is Shape.MAPPING: - sub_field_schema = field_singleton_schema(field, by_alias) - if sub_field_schema: + f_schema, f_definitions = field_singleton_schema( + field, by_alias=by_alias, model_name_map=model_name_map + ) + definitions.update(f_definitions) + if f_schema: # The dict values are not simply Any - return { - 'type': 'object', - 'additionalProperties': sub_field_schema, - } + return {'type': 'object', 'additionalProperties': f_schema}, definitions else: # The dict values are Any, no need to declare it - return { - 'type': 'object' - } + return {'type': 'object'}, definitions elif field.shape is Shape.TUPLE: - sub_schema = [field_type_schema(sf, by_alias) for sf in field.sub_fields] + sub_schema = [] + for sf in field.sub_fields: + sf_schema, sf_definitions = field_type_schema( + sf, by_alias=by_alias, model_name_map=model_name_map + ) + definitions.update(sf_definitions) + sub_schema.append(sf_schema) if len(sub_schema) == 1: sub_schema = sub_schema[0] - return { - 'type': 'array', - 'items': sub_schema, - } + return {'type': 'array', 'items': sub_schema}, definitions else: assert field.shape is Shape.SINGLETON, field.shape - return field_singleton_schema(field, by_alias) + f_schema, f_definitions = field_singleton_schema( + field, + by_alias=by_alias, + model_name_map=model_name_map, + schema_overrides=schema_overrides, + ) + definitions.update(f_definitions) + return f_schema, definitions -def model_schema(class_: 'main.BaseModel', by_alias=True) -> Dict[str, Any]: +def model_process_schema( + class_: Type['main.BaseModel'], + *, + by_alias=True, + model_name_map: Dict[Type['main.BaseModel'], str], +) -> Tuple[Dict[str, Any], Dict[str, Any]]: s = {'title': class_.__config__.title or class_.__name__} if class_.__doc__: s['description'] = clean_docstring(class_.__doc__) + m_schema, m_definitions = model_type_schema( + class_, by_alias=by_alias, model_name_map=model_name_map + ) + s.update(m_schema) + return s, m_definitions - s.update(model_type_schema(class_, by_alias)) - return s - -def model_type_schema(class_: 'main.BaseModel', by_alias: bool): - if by_alias: - properties = {f.alias: field_schema(f, by_alias) for f in class_.__fields__.values()} - required = [f.alias for f in class_.__fields__.values() if f.required] - else: - properties = {k: field_schema(f, by_alias) for k, f in class_.__fields__.items()} - required = [k for k, f in class_.__fields__.items() if f.required] +def model_type_schema( + class_: 'main.BaseModel', + *, + by_alias: bool, + model_name_map: Dict[Type['main.BaseModel'], str], +): + properties = {} + required = [] + definitions = {} + for k, f in class_.__fields__.items(): + f_schema, f_definitions = field_schema( + f, by_alias=by_alias, model_name_map=model_name_map + ) + definitions.update(f_definitions) + if by_alias: + properties[f.alias] = f_schema + if f.required: + required.append(f.alias) + else: + properties[k] = f_schema + if f.required: + required.append(k) out_schema = {'type': 'object', 'properties': properties} if required: out_schema['required'] = required - return out_schema + return out_schema, definitions -def field_singleton_schema(field: Field, by_alias: bool): +def field_singleton_schema( + field: Field, + *, + by_alias: bool, + model_name_map: Dict[Type['main.BaseModel'], str], + schema_overrides=False, +) -> Tuple[Dict[str, Any], Dict[str, Any]]: + definitions = {} if field.sub_fields: if len(field.sub_fields) == 1: - return field_type_schema(field.sub_fields[0], by_alias) + return field_type_schema( + field.sub_fields[0], by_alias=by_alias, model_name_map=model_name_map + ) else: - return {'anyOf': [field_type_schema(sf, by_alias) for sf in field.sub_fields]} + sub_field_schemas = [] + for sf in field.sub_fields: + sub_schema, sub_definitions = field_type_schema( + sf, by_alias=by_alias, model_name_map=model_name_map + ) + definitions.update(sub_definitions) + sub_field_schemas.append(sub_schema) + return {'anyOf': sub_field_schemas}, definitions if field.type_ is Any: - return {} # no restrictions - schema_value = {} + return {}, definitions # no restrictions + f_schema = {} if issubclass(field.type_, Enum): - schema_value.update({'enum': [item.value for item in field.type_]}) + f_schema.update({'enum': [item.value for item in field.type_]}) # Don't return immediately, to allow adding specific types # For constrained strings if hasattr(field.type_, 'min_length'): if field.type_.min_length is not None: - schema_value.update({'minLength': field.type_.min_length}) + f_schema.update({'minLength': field.type_.min_length}) if hasattr(field.type_, 'max_length'): if field.type_.max_length is not None: - schema_value.update({'maxLength': field.type_.max_length}) + f_schema.update({'maxLength': field.type_.max_length}) if hasattr(field.type_, 'regex'): if field.type_.regex is not None: - schema_value.update({'pattern': field.type_.regex.pattern}) + f_schema.update({'pattern': field.type_.regex.pattern}) # For constrained numbers if hasattr(field.type_, 'gt'): if field.type_.gt is not None: - schema_value.update({'exclusiveMinimum': field.type_.gt}) + f_schema.update({'exclusiveMinimum': field.type_.gt}) if hasattr(field.type_, 'lt'): if field.type_.lt is not None: - schema_value.update({'exclusiveMaximum': field.type_.lt}) + f_schema.update({'exclusiveMaximum': field.type_.lt}) if hasattr(field.type_, 'ge'): if field.type_.ge is not None: - schema_value.update({'minimum': field.type_.ge}) + f_schema.update({'minimum': field.type_.ge}) if hasattr(field.type_, 'le'): if field.type_.le is not None: - schema_value.update({'maximum': field.type_.le}) + f_schema.update({'maximum': field.type_.le}) # Sub-classes of str must go before str if issubclass(field.type_, EmailStr): - schema_value.update({'type': 'string', 'format': 'email'}) - return schema_value - if issubclass(field.type_, UrlStr): - schema_value.update({'type': 'string', 'format': 'uri'}) - return schema_value - if issubclass(field.type_, DSN): - schema_value.update({'type': 'string', 'format': 'dsn'}) - return schema_value - if issubclass(field.type_, str): - schema_value.update({'type': 'string'}) - return schema_value + f_schema.update({'type': 'string', 'format': 'email'}) + elif issubclass(field.type_, UrlStr): + f_schema.update({'type': 'string', 'format': 'uri'}) + elif issubclass(field.type_, DSN): + f_schema.update({'type': 'string', 'format': 'dsn'}) + elif issubclass(field.type_, str): + f_schema.update({'type': 'string'}) elif issubclass(field.type_, bytes): - schema_value.update({'type': 'string', 'format': 'binary'}) - return schema_value + f_schema.update({'type': 'string', 'format': 'binary'}) elif issubclass(field.type_, bool): - schema_value.update({'type': 'boolean'}) - return schema_value + f_schema.update({'type': 'boolean'}) elif issubclass(field.type_, int): - schema_value.update({'type': 'integer'}) - return schema_value + f_schema.update({'type': 'integer'}) elif issubclass(field.type_, float): - schema_value.update({'type': 'number'}) - return schema_value + f_schema.update({'type': 'number'}) elif issubclass(field.type_, Decimal): - schema_value.update({'type': 'number'}) - return schema_value + f_schema.update({'type': 'number'}) elif issubclass(field.type_, UUID1): - schema_value.update({'type': 'string', 'format': 'uuid1'}) - return schema_value + f_schema.update({'type': 'string', 'format': 'uuid1'}) elif issubclass(field.type_, UUID3): - schema_value.update({'type': 'string', 'format': 'uuid3'}) - return schema_value + f_schema.update({'type': 'string', 'format': 'uuid3'}) elif issubclass(field.type_, UUID4): - schema_value.update({'type': 'string', 'format': 'uuid4'}) - return schema_value + f_schema.update({'type': 'string', 'format': 'uuid4'}) elif issubclass(field.type_, UUID5): - schema_value.update({'type': 'string', 'format': 'uuid5'}) - return schema_value + f_schema.update({'type': 'string', 'format': 'uuid5'}) elif issubclass(field.type_, UUID): - schema_value.update({'type': 'string', 'format': 'uuid'}) - return schema_value + f_schema.update({'type': 'string', 'format': 'uuid'}) elif issubclass(field.type_, NameEmail): - schema_value.update({'type': 'string', 'format': 'name-email'}) - return schema_value + f_schema.update({'type': 'string', 'format': 'name-email'}) # This is the last value that can also be an Enum - if schema_value: - return schema_value + if f_schema: + return f_schema, definitions # Path subclasses must go before Path elif issubclass(field.type_, FilePath): - return {'type': 'string', 'format': 'file-path'} + return {'type': 'string', 'format': 'file-path'}, definitions elif issubclass(field.type_, DirectoryPath): - return {'type': 'string', 'format': 'directory-path'} + return {'type': 'string', 'format': 'directory-path'}, definitions elif issubclass(field.type_, Path): - return {'type': 'string', 'format': 'path'} + return {'type': 'string', 'format': 'path'}, definitions elif issubclass(field.type_, datetime): - return {'type': 'string', 'format': 'date-time'} + return {'type': 'string', 'format': 'date-time'}, definitions elif issubclass(field.type_, date): - return {'type': 'string', 'format': 'date'} + return {'type': 'string', 'format': 'date'}, definitions elif issubclass(field.type_, time): - return {'type': 'string', 'format': 'time'} + return {'type': 'string', 'format': 'time'}, definitions elif issubclass(field.type_, timedelta): - return {'type': 'string', 'format': 'time-delta'} + return {'type': 'string', 'format': 'time-delta'}, definitions elif issubclass(field.type_, Json): - return {'type': 'string', 'format': 'json-string'} + return {'type': 'string', 'format': 'json-string'}, definitions elif issubclass(field.type_, main.BaseModel): - return model_schema(field.type_, by_alias) + sub_schema, sub_definitions = model_process_schema( + field.type_, by_alias=by_alias, model_name_map=model_name_map + ) + definitions.update(sub_definitions) + if not schema_overrides: + model_name = model_name_map[field.type_] + definitions[model_name] = sub_schema + return {'$ref': f'#/definitions/{model_name}'}, definitions + else: + return sub_schema, definitions raise ValueError('Value not declarable with JSON Schema') + + +def model_schema(class_: 'main.BaseModel', by_alias=True) -> Dict[str, Any]: + flat_models = get_flat_models_from_model(class_) + _, model_name_map = get_model_name_maps(flat_models) + m_schema, m_definitions = model_process_schema( + class_, by_alias=by_alias, model_name_map=model_name_map + ) + if m_definitions: + m_schema.update({'definitions': m_definitions}) + return m_schema + + +def schema( + models: Sequence[Type['main.BaseModel']], + *, + by_alias=True, + title=None, + description=None, +) -> Dict: + flat_models = get_flat_models_from_models(models) + _, model_name_map = get_model_name_maps(flat_models) + definitions = {} + output_schema = {} + if title: + output_schema['title'] = title + if description: + output_schema['description'] = description + for model in models: + m_schema, m_definitions = model_process_schema( + model, by_alias=by_alias, model_name_map=model_name_map + ) + definitions.update(m_definitions) + model_name = model_name_map[model] + definitions[model_name] = m_schema + if definitions: + output_schema['definitions'] = definitions + return output_schema From c88a760ea84da8f387b3e428e3bd1740564ad819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Nov 2018 20:18:14 +0400 Subject: [PATCH 08/65] Update and augment JSON Schema tests to include definitions and refs and generation of a single JSON Schema with definitions from multiple (unrelated) models --- tests/test_schema.py | 247 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 218 insertions(+), 29 deletions(-) diff --git a/tests/test_schema.py b/tests/test_schema.py index 120186dd0e..35b777dff6 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -8,11 +8,16 @@ import pytest from pydantic import BaseModel, Schema, ValidationError +from pydantic.schema import get_flat_models_from_model, get_flat_models_from_models, get_model_name_maps, schema from pydantic.types import (DSN, UUID1, UUID3, UUID4, UUID5, ConstrainedDecimal, ConstrainedFloat, ConstrainedInt, ConstrainedStr, DirectoryPath, EmailStr, FilePath, Json, NameEmail, NegativeFloat, NegativeInt, NoneBytes, NoneStr, NoneStrBytes, PositiveFloat, PositiveInt, StrBytes, StrictStr, UrlStr, condecimal, confloat, conint, constr, urlstr) +from .schema_test_package.modulea.modela import Model as ModelA +from .schema_test_package.moduleb.modelb import Model as ModelB +from .schema_test_package.modulec.modelc import Model as ModelC + def test_key(): class ApplePie(BaseModel): @@ -80,15 +85,18 @@ class Bar(BaseModel): assert Bar.schema() == { 'type': 'object', 'title': 'Bar', - 'properties': { - 'a': {'type': 'integer', 'title': 'A'}, - 'b': { + 'definitions': { + 'Foo': { 'type': 'object', 'title': 'Foo', 'description': 'hello', 'properties': {'b': {'type': 'number', 'title': 'B'}}, 'required': ['b'], - }, + } + }, + 'properties': { + 'a': {'type': 'integer', 'title': 'A'}, + 'b': {'$ref': '#/definitions/Foo'}, }, 'required': ['a'], } @@ -158,9 +166,6 @@ class Model(BaseModel): a = b'foobar' b = Decimal('12.34') - with pytest.raises(TypeError): - json.dumps(Model.schema()) - assert Model.schema_json(indent=2) == ( '{\n' ' "title": "Model",\n' @@ -192,18 +197,17 @@ class Bar(BaseModel): assert Bar.schema() == { 'title': 'Bar', 'type': 'object', - 'properties': { - 'b': { - 'type': 'array', - 'items': { - 'title': 'Foo', - 'type': 'object', - 'properties': {'a': {'type': 'number', 'title': 'A'}}, - 'required': ['a'], - }, - 'title': 'B', + 'definitions': { + 'Foo': { + 'title': 'Foo', + 'type': 'object', + 'properties': {'a': {'type': 'number', 'title': 'A'}}, + 'required': ['a'], } }, + 'properties': { + 'b': {'type': 'array', 'items': {'$ref': '#/definitions/Foo'}, 'title': 'B'} + }, 'required': ['b'], } @@ -297,25 +301,23 @@ class Model(BaseModel): 'title': 'Model', 'description': 'party time', 'type': 'object', + 'definitions': { + 'Foo': { + 'title': 'Foo', + 'type': 'object', + 'properties': {'a': {'title': 'A', 'type': 'number'}}, + 'required': ['a'], + } + }, 'properties': { 'a': {'title': 'A', 'anyOf': [{'type': 'integer'}, {'type': 'string'}]}, 'b': {'title': 'B', 'type': 'array', 'items': {'type': 'integer'}}, 'c': { 'title': 'C', 'type': 'object', - 'additionalProperties': { - 'title': 'Foo', - 'type': 'object', - 'properties': {'a': {'title': 'A', 'type': 'number'}}, - 'required': ['a'], - }, - }, - 'd': { - 'title': 'Foo', - 'type': 'object', - 'properties': {'a': {'title': 'A', 'type': 'number'}}, - 'required': ['a'], + 'additionalProperties': {'$ref': '#/definitions/Foo'}, }, + 'd': {'$ref': '#/definitions/Foo'}, 'e': {'title': 'E', 'type': 'object'}, }, 'required': ['a', 'b', 'c', 'e'], @@ -548,3 +550,190 @@ class Model(BaseModel): 'properties': {'a': {'title': 'A', 'type': 'string', 'format': 'json-string'}}, 'required': ['a'], } + + +def test_flat_models_unique_models(): + flat_models = get_flat_models_from_models([ModelA, ModelB, ModelC]) + assert flat_models == set([ModelA, ModelB]) + + +def test_flat_models_with_submodels(): + class Foo(BaseModel): + a: str + + class Bar(BaseModel): + b: List[Foo] + + class Baz(BaseModel): + c: Dict[str, Bar] + + flat_models = get_flat_models_from_model(Baz) + assert flat_models == set([Foo, Bar, Baz]) + + +def test_flat_models_with_submodels_from_sequence(): + class Foo(BaseModel): + a: str + + class Bar(BaseModel): + b: Foo + + class Baz(BaseModel): + c: Bar + + class Ingredient(BaseModel): + name: str + + class Pizza(BaseModel): + name: str + ingredients: List[Ingredient] + + flat_models = get_flat_models_from_models([Baz, Pizza]) + assert flat_models == set([Foo, Bar, Baz, Ingredient, Pizza]) + + +def test_model_name_maps(): + class Foo(BaseModel): + a: str + + class Bar(BaseModel): + b: Foo + + class Baz(BaseModel): + c: Bar + + flat_models = get_flat_models_from_models([Baz, ModelA, ModelB, ModelC]) + name_model_map, model_name_map = get_model_name_maps(flat_models) + assert name_model_map == { + 'Foo': Foo, + 'Bar': Bar, + 'Baz': Baz, + 'tests__schema_test_package__modulea__modela__Model': ModelA, + 'tests__schema_test_package__moduleb__modelb__Model': ModelB, + } + assert model_name_map == { + Foo: 'Foo', + Bar: 'Bar', + Baz: 'Baz', + ModelA: 'tests__schema_test_package__modulea__modela__Model', + ModelB: 'tests__schema_test_package__moduleb__modelb__Model', + } + + +def test_schema_overrides(): + class Foo(BaseModel): + a: str + + class Bar(BaseModel): + b: Foo = Foo(a='foo') + + class Baz(BaseModel): + c: Bar + + class Model(BaseModel): + d: Baz + + model_schema = Model.schema() + assert model_schema == { + 'title': 'Model', + 'type': 'object', + 'definitions': { + 'Bar': { + 'title': 'Bar', + 'type': 'object', + 'properties': { + 'b': { + 'title': 'Foo', + 'type': 'object', + 'properties': {'a': {'title': 'A', 'type': 'string'}}, + 'required': ['a'], + 'default': {'a': 'foo'}, + } + }, + }, + 'Baz': { + 'title': 'Baz', + 'type': 'object', + 'properties': {'c': {'$ref': '#/definitions/Bar'}}, + 'required': ['c'], + }, + }, + 'properties': {'d': {'$ref': '#/definitions/Baz'}}, + 'required': ['d'], + } + + +def test_schema_from_models(): + class Foo(BaseModel): + a: str + + class Bar(BaseModel): + b: Foo + + class Baz(BaseModel): + c: Bar + + class Model(BaseModel): + d: Baz + + class Ingredient(BaseModel): + name: str + + class Pizza(BaseModel): + name: str + ingredients: List[Ingredient] + + model_schema = schema( + [Model, Pizza], + title='Multi-model schema', + description='Single JSON Schema with multiple definitions', + ) + assert model_schema == { + 'title': 'Multi-model schema', + 'description': 'Single JSON Schema with multiple definitions', + 'definitions': { + 'Pizza': { + 'title': 'Pizza', + 'type': 'object', + 'properties': { + 'name': {'title': 'Name', 'type': 'string'}, + 'ingredients': { + 'title': 'Ingredients', + 'type': 'array', + 'items': {'$ref': '#/definitions/Ingredient'}, + }, + }, + 'required': ['name', 'ingredients'], + }, + 'Ingredient': { + 'title': 'Ingredient', + 'type': 'object', + 'properties': {'name': {'title': 'Name', 'type': 'string'}}, + 'required': ['name'], + }, + 'Model': { + 'title': 'Model', + 'type': 'object', + 'properties': {'d': {'$ref': '#/definitions/Baz'}}, + 'required': ['d'], + }, + 'Baz': { + 'title': 'Baz', + 'type': 'object', + 'properties': {'c': {'$ref': '#/definitions/Bar'}}, + 'required': ['c'], + }, + 'Bar': { + 'title': 'Bar', + 'type': 'object', + 'properties': {'b': {'$ref': '#/definitions/Foo'}}, + 'required': ['b'], + }, + 'Foo': { + 'title': 'Foo', + 'type': 'object', + 'properties': {'a': {'title': 'A', 'type': 'string'}}, + 'required': ['a'], + }, + }, + } From b24bb29903733082355c780e9ace0eec9e7b6de7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Nov 2018 21:12:42 +0400 Subject: [PATCH 09/65] Add ref_prefix functionality to JSON Schema generation functions --- pydantic/schema.py | 65 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index 4d36d069e7..484b219e93 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -80,7 +80,11 @@ def get_model_name_maps( def field_schema( - field: Field, *, by_alias=True, model_name_map: Dict[Type['main.BaseModel'], str] + field: Field, + *, + by_alias=True, + model_name_map: Dict[Type['main.BaseModel'], str], + ref_prefix='#/definitions/', ) -> Tuple[Dict[str, Any], Dict[str, Any]]: schema_overrides = False s = dict(title=field._schema.title or field.alias.title()) @@ -102,6 +106,7 @@ def field_schema( by_alias=by_alias, model_name_map=model_name_map, schema_overrides=schema_overrides, + ref_prefix=ref_prefix, ) # $ref will only be returned when there are no schema_overrides if '$ref' in f_schema: @@ -117,23 +122,33 @@ def field_type_schema( by_alias: bool, model_name_map: Dict[Type['main.BaseModel'], str], schema_overrides=False, + ref_prefix='#/definitions/', ) -> Tuple[Dict[str, Any], Dict[str, Any]]: definitions = {} if field.shape is Shape.LIST: f_schema, f_definitions = field_singleton_schema( - field, by_alias=by_alias, model_name_map=model_name_map + field, + by_alias=by_alias, + model_name_map=model_name_map, + ref_prefix=ref_prefix, ) definitions.update(f_definitions) return {'type': 'array', 'items': f_schema}, definitions elif field.shape is Shape.SET: f_schema, f_definitions = field_singleton_schema( - field, by_alias=by_alias, model_name_map=model_name_map + field, + by_alias=by_alias, + model_name_map=model_name_map, + ref_prefix=ref_prefix, ) definitions.update(f_definitions) return {'type': 'array', 'uniqueItems': True, 'items': f_schema}, definitions elif field.shape is Shape.MAPPING: f_schema, f_definitions = field_singleton_schema( - field, by_alias=by_alias, model_name_map=model_name_map + field, + by_alias=by_alias, + model_name_map=model_name_map, + ref_prefix=ref_prefix, ) definitions.update(f_definitions) if f_schema: @@ -146,7 +161,10 @@ def field_type_schema( sub_schema = [] for sf in field.sub_fields: sf_schema, sf_definitions = field_type_schema( - sf, by_alias=by_alias, model_name_map=model_name_map + sf, + by_alias=by_alias, + model_name_map=model_name_map, + ref_prefix=ref_prefix, ) definitions.update(sf_definitions) sub_schema.append(sf_schema) @@ -160,6 +178,7 @@ def field_type_schema( by_alias=by_alias, model_name_map=model_name_map, schema_overrides=schema_overrides, + ref_prefix=ref_prefix, ) definitions.update(f_definitions) return f_schema, definitions @@ -170,12 +189,13 @@ def model_process_schema( *, by_alias=True, model_name_map: Dict[Type['main.BaseModel'], str], + ref_prefix='#/definitions/', ) -> Tuple[Dict[str, Any], Dict[str, Any]]: s = {'title': class_.__config__.title or class_.__name__} if class_.__doc__: s['description'] = clean_docstring(class_.__doc__) m_schema, m_definitions = model_type_schema( - class_, by_alias=by_alias, model_name_map=model_name_map + class_, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix ) s.update(m_schema) return s, m_definitions @@ -186,13 +206,14 @@ def model_type_schema( *, by_alias: bool, model_name_map: Dict[Type['main.BaseModel'], str], + ref_prefix='#/definitions/', ): properties = {} required = [] definitions = {} for k, f in class_.__fields__.items(): f_schema, f_definitions = field_schema( - f, by_alias=by_alias, model_name_map=model_name_map + f, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix ) definitions.update(f_definitions) if by_alias: @@ -215,18 +236,25 @@ def field_singleton_schema( by_alias: bool, model_name_map: Dict[Type['main.BaseModel'], str], schema_overrides=False, + ref_prefix='#/definitions/', ) -> Tuple[Dict[str, Any], Dict[str, Any]]: definitions = {} if field.sub_fields: if len(field.sub_fields) == 1: return field_type_schema( - field.sub_fields[0], by_alias=by_alias, model_name_map=model_name_map + field.sub_fields[0], + by_alias=by_alias, + model_name_map=model_name_map, + ref_prefix=ref_prefix, ) else: sub_field_schemas = [] for sf in field.sub_fields: sub_schema, sub_definitions = field_type_schema( - sf, by_alias=by_alias, model_name_map=model_name_map + sf, + by_alias=by_alias, + model_name_map=model_name_map, + ref_prefix=ref_prefix, ) definitions.update(sub_definitions) sub_field_schemas.append(sub_schema) @@ -313,23 +341,28 @@ def field_singleton_schema( return {'type': 'string', 'format': 'json-string'}, definitions elif issubclass(field.type_, main.BaseModel): sub_schema, sub_definitions = model_process_schema( - field.type_, by_alias=by_alias, model_name_map=model_name_map + field.type_, + by_alias=by_alias, + model_name_map=model_name_map, + ref_prefix=ref_prefix, ) definitions.update(sub_definitions) if not schema_overrides: model_name = model_name_map[field.type_] definitions[model_name] = sub_schema - return {'$ref': f'#/definitions/{model_name}'}, definitions + return {'$ref': f'{ref_prefix}{model_name}'}, definitions else: return sub_schema, definitions raise ValueError('Value not declarable with JSON Schema') -def model_schema(class_: 'main.BaseModel', by_alias=True) -> Dict[str, Any]: +def model_schema( + class_: 'main.BaseModel', by_alias=True, ref_prefix='#/definitions/' +) -> Dict[str, Any]: flat_models = get_flat_models_from_model(class_) _, model_name_map = get_model_name_maps(flat_models) m_schema, m_definitions = model_process_schema( - class_, by_alias=by_alias, model_name_map=model_name_map + class_, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix ) if m_definitions: m_schema.update({'definitions': m_definitions}) @@ -342,6 +375,7 @@ def schema( by_alias=True, title=None, description=None, + ref_prefix='#/definitions/', ) -> Dict: flat_models = get_flat_models_from_models(models) _, model_name_map = get_model_name_maps(flat_models) @@ -353,7 +387,10 @@ def schema( output_schema['description'] = description for model in models: m_schema, m_definitions = model_process_schema( - model, by_alias=by_alias, model_name_map=model_name_map + model, + by_alias=by_alias, + model_name_map=model_name_map, + ref_prefix=ref_prefix, ) definitions.update(m_definitions) model_name = model_name_map[model] From 580cc28dee304a0f3e40a0be8a012598e7570129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Nov 2018 21:13:33 +0400 Subject: [PATCH 10/65] Test custom ref_prefix in JSON Schema generation --- tests/test_schema.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/test_schema.py b/tests/test_schema.py index 35b777dff6..1d52e5df0c 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -737,3 +737,45 @@ class Pizza(BaseModel): }, }, } + + +def test_schema_with_ref_prefix(): + class Foo(BaseModel): + a: str + + class Bar(BaseModel): + b: Foo + + class Baz(BaseModel): + c: Bar + + model_schema = schema( + [Bar, Baz], + title='Multi-model schema', + description='Custom prefix for $ref fields, moving the definitions to the correspondig location has to be done by the developer', + ref_prefix='#/components/schemas/', # OpenAPI style + ) + assert model_schema == { + 'title': 'Multi-model schema', + 'description': 'Custom prefix for $ref fields, moving the definitions to the correspondig location has to be done by the developer', + 'definitions': { + 'Baz': { + 'title': 'Baz', + 'type': 'object', + 'properties': {'c': {'$ref': '#/components/schemas/Bar'}}, + 'required': ['c'], + }, + 'Bar': { + 'title': 'Bar', + 'type': 'object', + 'properties': {'b': {'$ref': '#/components/schemas/Foo'}}, + 'required': ['b'], + }, + 'Foo': { + 'title': 'Foo', + 'type': 'object', + 'properties': {'a': {'title': 'A', 'type': 'string'}}, + 'required': ['a'], + }, + }, + } From 0d18d7deb197d754c06d72e70f77499e9744b618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Nov 2018 21:14:08 +0400 Subject: [PATCH 11/65] Remove un-used BaseModel method, now refactored to schema module --- pydantic/main.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pydantic/main.py b/pydantic/main.py index 16104f3622..69e5b18a05 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -318,10 +318,6 @@ def copy( def fields(self): return self.__fields__ - @classmethod - def type_schema(cls, by_alias): - return model_type_schema(cls, by_alias=by_alias) - @classmethod def schema(cls, by_alias=True) -> Dict[str, Any]: cached = cls._schema_cache.get(by_alias) From 1ec4aba59e3c6cd4086bdce7dd8da17ef4630d2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Nov 2018 22:34:27 +0400 Subject: [PATCH 12/65] Update formating of test_schema --- tests/test_schema.py | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/tests/test_schema.py b/tests/test_schema.py index 1d52e5df0c..4dc2337fab 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -9,10 +9,37 @@ from pydantic import BaseModel, Schema, ValidationError from pydantic.schema import get_flat_models_from_model, get_flat_models_from_models, get_model_name_maps, schema -from pydantic.types import (DSN, UUID1, UUID3, UUID4, UUID5, ConstrainedDecimal, ConstrainedFloat, ConstrainedInt, - ConstrainedStr, DirectoryPath, EmailStr, FilePath, Json, NameEmail, NegativeFloat, - NegativeInt, NoneBytes, NoneStr, NoneStrBytes, PositiveFloat, PositiveInt, StrBytes, - StrictStr, UrlStr, condecimal, confloat, conint, constr, urlstr) +from pydantic.types import ( + DSN, + UUID1, + UUID3, + UUID4, + UUID5, + ConstrainedDecimal, + ConstrainedFloat, + ConstrainedInt, + ConstrainedStr, + DirectoryPath, + EmailStr, + FilePath, + Json, + NameEmail, + NegativeFloat, + NegativeInt, + NoneBytes, + NoneStr, + NoneStrBytes, + PositiveFloat, + PositiveInt, + StrBytes, + StrictStr, + UrlStr, + condecimal, + confloat, + conint, + constr, + urlstr, +) from .schema_test_package.modulea.modela import Model as ModelA from .schema_test_package.moduleb.modelb import Model as ModelB From 115883bdf13c08273f3c900caa245b523b707528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Nov 2018 22:58:14 +0400 Subject: [PATCH 13/65] Fix long lines in test_schema --- tests/test_schema.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_schema.py b/tests/test_schema.py index 4dc2337fab..0c88a028d8 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -1,4 +1,3 @@ -import json from datetime import date, datetime, time, timedelta from decimal import Decimal from enum import Enum, IntEnum @@ -779,12 +778,12 @@ class Baz(BaseModel): model_schema = schema( [Bar, Baz], title='Multi-model schema', - description='Custom prefix for $ref fields, moving the definitions to the correspondig location has to be done by the developer', + description='Custom prefix for $ref fields', ref_prefix='#/components/schemas/', # OpenAPI style ) assert model_schema == { 'title': 'Multi-model schema', - 'description': 'Custom prefix for $ref fields, moving the definitions to the correspondig location has to be done by the developer', + 'description': 'Custom prefix for $ref fields', 'definitions': { 'Baz': { 'title': 'Baz', From 343b5040933a5a213c6f60be38045706585c0593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Nov 2018 22:58:36 +0400 Subject: [PATCH 14/65] Fix imported but unused in fields --- pydantic/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pydantic/fields.py b/pydantic/fields.py index 34c06e23dc..670bb5a647 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -1,6 +1,6 @@ import inspect -from enum import Enum, IntEnum -from typing import Any, Callable, List, Mapping, NamedTuple, Pattern, Set, Tuple, Type, Union +from enum import IntEnum +from typing import Any, Callable, List, Mapping, NamedTuple, Set, Tuple, Type, Union from . import errors as errors_ from .error_wrappers import ErrorWrapper From 0391d62fcd5b0a10abb4946b5a0d28e439afec7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Nov 2018 22:59:00 +0400 Subject: [PATCH 15/65] Fix imported but unused in main.py --- pydantic/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pydantic/main.py b/pydantic/main.py index 69e5b18a05..44cfd8def6 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -14,10 +14,10 @@ from .json import custom_pydantic_encoder, pydantic_encoder from .parse import Protocol, load_file, load_str_bytes from .types import StrBytes -from .utils import clean_docstring, truncate, validate_field_name +from .utils import truncate, validate_field_name from .validators import dict_validator -from .schema import model_schema, model_type_schema +from .schema import model_schema class BaseConfig: From fcf34a40142ae8f845d7ae5b8f811205c34939dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Nov 2018 22:59:18 +0400 Subject: [PATCH 16/65] Ignore imported but unused for testing modulec --- tests/schema_test_package/modulec/modelc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/schema_test_package/modulec/modelc.py b/tests/schema_test_package/modulec/modelc.py index 5085fb08f0..aed2198f4c 100644 --- a/tests/schema_test_package/modulec/modelc.py +++ b/tests/schema_test_package/modulec/modelc.py @@ -1 +1 @@ -from ..moduleb.modelb import Model +from ..moduleb.modelb import Model # noqa: F401 (ignore imported but unused) From 5c642cbf423f7af7feb20991948e8a388d82ea75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Nov 2018 22:59:56 +0400 Subject: [PATCH 17/65] Refactor schema module for complexity --- pydantic/schema.py | 72 +++++++++++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index 484b219e93..9901fd1a5d 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -2,13 +2,25 @@ from decimal import Decimal from enum import Enum from pathlib import Path -from typing import Any, Dict, Mapping, Sequence, Set, Tuple, Type +from typing import Any, Dict, Sequence, Set, Tuple, Type from uuid import UUID from . import main from .fields import Field, Shape from .json import pydantic_encoder -from .types import DSN, UUID1, UUID3, UUID4, UUID5, DirectoryPath, EmailStr, FilePath, Json, NameEmail, UrlStr +from .types import ( + DSN, + UUID1, + UUID3, + UUID4, + UUID5, + DirectoryPath, + EmailStr, + FilePath, + Json, + NameEmail, + UrlStr, +) from .utils import clean_docstring @@ -230,8 +242,8 @@ def model_type_schema( return out_schema, definitions -def field_singleton_schema( - field: Field, +def field_singleton_sub_fields_schema( + sub_fields: Sequence[Field], *, by_alias: bool, model_name_map: Dict[Type['main.BaseModel'], str], @@ -239,26 +251,46 @@ def field_singleton_schema( ref_prefix='#/definitions/', ) -> Tuple[Dict[str, Any], Dict[str, Any]]: definitions = {} - if field.sub_fields: - if len(field.sub_fields) == 1: - return field_type_schema( - field.sub_fields[0], + if len(sub_fields) == 1: + return field_type_schema( + sub_fields[0], + by_alias=by_alias, + model_name_map=model_name_map, + schema_overrides=schema_overrides, + ref_prefix=ref_prefix, + ) + else: + sub_field_schemas = [] + for sf in sub_fields: + sub_schema, sub_definitions = field_type_schema( + sf, by_alias=by_alias, model_name_map=model_name_map, + schema_overrides=schema_overrides, ref_prefix=ref_prefix, ) - else: - sub_field_schemas = [] - for sf in field.sub_fields: - sub_schema, sub_definitions = field_type_schema( - sf, - by_alias=by_alias, - model_name_map=model_name_map, - ref_prefix=ref_prefix, - ) - definitions.update(sub_definitions) - sub_field_schemas.append(sub_schema) - return {'anyOf': sub_field_schemas}, definitions + definitions.update(sub_definitions) + sub_field_schemas.append(sub_schema) + return {'anyOf': sub_field_schemas}, definitions + + +def field_singleton_schema( + field: Field, + *, + by_alias: bool, + model_name_map: Dict[Type['main.BaseModel'], str], + schema_overrides=False, + ref_prefix='#/definitions/', +) -> Tuple[Dict[str, Any], Dict[str, Any]]: + definitions = {} + if field.sub_fields: + return field_singleton_sub_fields_schema( + field.sub_fields, + by_alias=by_alias, + model_name_map=model_name_map, + schema_overrides=schema_overrides, + ref_prefix=ref_prefix, + ) if field.type_ is Any: return {}, definitions # no restrictions f_schema = {} From 56b377b77602a17dffbf3a61867203da37555494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 02:28:56 +0400 Subject: [PATCH 18/65] Add conflicting name model to raise coverage --- tests/test_schema.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_schema.py b/tests/test_schema.py index 0c88a028d8..32d815e5c3 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -43,6 +43,7 @@ from .schema_test_package.modulea.modela import Model as ModelA from .schema_test_package.moduleb.modelb import Model as ModelB from .schema_test_package.modulec.modelc import Model as ModelC +from .schema_test_package.moduled.modeld import Model as ModelD def test_key(): @@ -628,7 +629,7 @@ class Bar(BaseModel): class Baz(BaseModel): c: Bar - flat_models = get_flat_models_from_models([Baz, ModelA, ModelB, ModelC]) + flat_models = get_flat_models_from_models([Baz, ModelA, ModelB, ModelC, ModelD]) name_model_map, model_name_map = get_model_name_maps(flat_models) assert name_model_map == { 'Foo': Foo, @@ -636,6 +637,7 @@ class Baz(BaseModel): 'Baz': Baz, 'tests__schema_test_package__modulea__modela__Model': ModelA, 'tests__schema_test_package__moduleb__modelb__Model': ModelB, + 'tests__schema_test_package__moduled__modeld__Model': ModelD, } assert model_name_map == { Foo: 'Foo', @@ -643,6 +645,7 @@ class Baz(BaseModel): Baz: 'Baz', ModelA: 'tests__schema_test_package__modulea__modela__Model', ModelB: 'tests__schema_test_package__moduleb__modelb__Model', + ModelD: 'tests__schema_test_package__moduled__modeld__Model', } @@ -654,7 +657,7 @@ class Bar(BaseModel): b: Foo = Foo(a='foo') class Baz(BaseModel): - c: Bar + c: Optional[Bar] class Model(BaseModel): d: Baz @@ -681,7 +684,6 @@ class Model(BaseModel): 'title': 'Baz', 'type': 'object', 'properties': {'c': {'$ref': '#/definitions/Bar'}}, - 'required': ['c'], }, }, 'properties': {'d': {'$ref': '#/definitions/Baz'}}, From fd8ffc9e614e6ae5a96029389a23022a6153f127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 02:29:43 +0400 Subject: [PATCH 19/65] Add conflicting model to test other flow and raise coverage --- tests/schema_test_package/moduled/__init__.py | 0 tests/schema_test_package/moduled/modeld.py | 5 +++++ 2 files changed, 5 insertions(+) create mode 100644 tests/schema_test_package/moduled/__init__.py create mode 100644 tests/schema_test_package/moduled/modeld.py diff --git a/tests/schema_test_package/moduled/__init__.py b/tests/schema_test_package/moduled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/schema_test_package/moduled/modeld.py b/tests/schema_test_package/moduled/modeld.py new file mode 100644 index 0000000000..81bcbfbde8 --- /dev/null +++ b/tests/schema_test_package/moduled/modeld.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class Model(BaseModel): + a: int From 4529c3a658c2521d37eedecde7a2a8543b22eade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 02:30:20 +0400 Subject: [PATCH 20/65] Ignore complexity as destructuring more would make it more complex and more difficult to understand, similar to .fields.validate --- pydantic/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index 9901fd1a5d..923a8a0a04 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -274,7 +274,7 @@ def field_singleton_sub_fields_schema( return {'anyOf': sub_field_schemas}, definitions -def field_singleton_schema( +def field_singleton_schema( # noqa: C901 (ignore complexity) field: Field, *, by_alias: bool, From a3055dcb56737646bf5d85d316a36adc4f1766f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 02:53:13 +0400 Subject: [PATCH 21/65] Fix import sorting --- pydantic/main.py | 3 +-- pydantic/schema.py | 14 +------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/pydantic/main.py b/pydantic/main.py index 44cfd8def6..621bfa5f63 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -13,12 +13,11 @@ from .fields import Field, Validator from .json import custom_pydantic_encoder, pydantic_encoder from .parse import Protocol, load_file, load_str_bytes +from .schema import model_schema from .types import StrBytes from .utils import truncate, validate_field_name from .validators import dict_validator -from .schema import model_schema - class BaseConfig: title = None diff --git a/pydantic/schema.py b/pydantic/schema.py index 923a8a0a04..c3abe5e507 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -8,19 +8,7 @@ from . import main from .fields import Field, Shape from .json import pydantic_encoder -from .types import ( - DSN, - UUID1, - UUID3, - UUID4, - UUID5, - DirectoryPath, - EmailStr, - FilePath, - Json, - NameEmail, - UrlStr, -) +from .types import DSN, UUID1, UUID3, UUID4, UUID5, DirectoryPath, EmailStr, FilePath, Json, NameEmail, UrlStr from .utils import clean_docstring From 549a85dc82fce11c55170bb3881fc9fa931b8243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 02:55:48 +0400 Subject: [PATCH 22/65] Update formatting with black, with CI settings --- pydantic/schema.py | 48 ++++------------- tests/test_schema.py | 119 +++++++------------------------------------ 2 files changed, 29 insertions(+), 138 deletions(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index c3abe5e507..b14fe9e39d 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -12,9 +12,7 @@ from .utils import clean_docstring -def get_flat_models_from_model( - model: Type['main.BaseModel'] -) -> Set[Type['main.BaseModel']]: +def get_flat_models_from_model(model: Type['main.BaseModel']) -> Set[Type['main.BaseModel']]: flat_models: Set[Type[main.BaseModel]] = set() assert issubclass(model, main.BaseModel) flat_models.add(model) @@ -39,9 +37,7 @@ def get_flat_models_from_sub_fields(fields) -> Set[Type['main.BaseModel']]: return flat_models -def get_flat_models_from_models( - models: Sequence[Type['main.BaseModel']] -) -> Set[Type['main.BaseModel']]: +def get_flat_models_from_models(models: Sequence[Type['main.BaseModel']]) -> Set[Type['main.BaseModel']]: flat_models: Set[Type[main.BaseModel]] = set() for model in models: flat_models = flat_models | get_flat_models_from_model(model) @@ -80,11 +76,7 @@ def get_model_name_maps( def field_schema( - field: Field, - *, - by_alias=True, - model_name_map: Dict[Type['main.BaseModel'], str], - ref_prefix='#/definitions/', + field: Field, *, by_alias=True, model_name_map: Dict[Type['main.BaseModel'], str], ref_prefix='#/definitions/' ) -> Tuple[Dict[str, Any], Dict[str, Any]]: schema_overrides = False s = dict(title=field._schema.title or field.alias.title()) @@ -127,28 +119,19 @@ def field_type_schema( definitions = {} if field.shape is Shape.LIST: f_schema, f_definitions = field_singleton_schema( - field, - by_alias=by_alias, - model_name_map=model_name_map, - ref_prefix=ref_prefix, + field, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix ) definitions.update(f_definitions) return {'type': 'array', 'items': f_schema}, definitions elif field.shape is Shape.SET: f_schema, f_definitions = field_singleton_schema( - field, - by_alias=by_alias, - model_name_map=model_name_map, - ref_prefix=ref_prefix, + field, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix ) definitions.update(f_definitions) return {'type': 'array', 'uniqueItems': True, 'items': f_schema}, definitions elif field.shape is Shape.MAPPING: f_schema, f_definitions = field_singleton_schema( - field, - by_alias=by_alias, - model_name_map=model_name_map, - ref_prefix=ref_prefix, + field, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix ) definitions.update(f_definitions) if f_schema: @@ -161,10 +144,7 @@ def field_type_schema( sub_schema = [] for sf in field.sub_fields: sf_schema, sf_definitions = field_type_schema( - sf, - by_alias=by_alias, - model_name_map=model_name_map, - ref_prefix=ref_prefix, + sf, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix ) definitions.update(sf_definitions) sub_schema.append(sf_schema) @@ -361,10 +341,7 @@ def field_singleton_schema( # noqa: C901 (ignore complexity) return {'type': 'string', 'format': 'json-string'}, definitions elif issubclass(field.type_, main.BaseModel): sub_schema, sub_definitions = model_process_schema( - field.type_, - by_alias=by_alias, - model_name_map=model_name_map, - ref_prefix=ref_prefix, + field.type_, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix ) definitions.update(sub_definitions) if not schema_overrides: @@ -376,9 +353,7 @@ def field_singleton_schema( # noqa: C901 (ignore complexity) raise ValueError('Value not declarable with JSON Schema') -def model_schema( - class_: 'main.BaseModel', by_alias=True, ref_prefix='#/definitions/' -) -> Dict[str, Any]: +def model_schema(class_: 'main.BaseModel', by_alias=True, ref_prefix='#/definitions/') -> Dict[str, Any]: flat_models = get_flat_models_from_model(class_) _, model_name_map = get_model_name_maps(flat_models) m_schema, m_definitions = model_process_schema( @@ -407,10 +382,7 @@ def schema( output_schema['description'] = description for model in models: m_schema, m_definitions = model_process_schema( - model, - by_alias=by_alias, - model_name_map=model_name_map, - ref_prefix=ref_prefix, + model, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix ) definitions.update(m_definitions) model_name = model_name_map[model] diff --git a/tests/test_schema.py b/tests/test_schema.py index 32d815e5c3..1aed96b619 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -57,10 +57,7 @@ class ApplePie(BaseModel): s = { 'type': 'object', - 'properties': { - 'a': {'type': 'number', 'title': 'A'}, - 'b': {'type': 'integer', 'title': 'B', 'default': 10}, - }, + 'properties': {'a': {'type': 'number', 'title': 'A'}, 'b': {'type': 'integer', 'title': 'B', 'default': 10}}, 'required': ['a'], 'title': 'ApplePie', 'description': 'This is a test.', @@ -92,10 +89,7 @@ class Config: 'required': ['Snap'], } assert ApplePie.schema() == s - assert list(ApplePie.schema(by_alias=True)['properties'].keys()) == [ - 'Snap', - 'Crackle', - ] + assert list(ApplePie.schema(by_alias=True)['properties'].keys()) == ['Snap', 'Crackle'] assert list(ApplePie.schema(by_alias=False)['properties'].keys()) == ['a', 'b'] @@ -121,10 +115,7 @@ class Bar(BaseModel): 'required': ['b'], } }, - 'properties': { - 'a': {'type': 'integer', 'title': 'A'}, - 'b': {'$ref': '#/definitions/Foo'}, - }, + 'properties': {'a': {'type': 'integer', 'title': 'A'}, 'b': {'$ref': '#/definitions/Foo'}}, 'required': ['a'], } @@ -145,11 +136,7 @@ class Model(BaseModel): 'title': 'Model', 'properties': { 'foo': {'type': 'integer', 'title': 'Foo is Great', 'default': 4}, - 'bar': { - 'type': 'string', - 'title': 'Bar', - 'description': 'this description of bar', - }, + 'bar': {'type': 'string', 'title': 'Bar', 'description': 'this description of bar'}, }, 'required': ['bar'], } @@ -232,9 +219,7 @@ class Bar(BaseModel): 'required': ['a'], } }, - 'properties': { - 'b': {'type': 'array', 'items': {'$ref': '#/definitions/Foo'}, 'title': 'B'} - }, + 'properties': {'b': {'type': 'array', 'items': {'$ref': '#/definitions/Foo'}, 'title': 'B'}}, 'required': ['b'], } @@ -243,11 +228,7 @@ def test_optional(): class Model(BaseModel): a: Optional[str] - assert Model.schema() == { - 'title': 'Model', - 'type': 'object', - 'properties': {'a': {'type': 'string', 'title': 'A'}}, - } + assert Model.schema() == {'title': 'Model', 'type': 'object', 'properties': {'a': {'type': 'string', 'title': 'A'}}} def test_any(): @@ -269,14 +250,7 @@ class Model(BaseModel): assert Model.schema() == { 'title': 'Model', 'type': 'object', - 'properties': { - 'a': { - 'title': 'A', - 'type': 'array', - 'uniqueItems': True, - 'items': {'type': 'integer'}, - } - }, + 'properties': {'a': {'title': 'A', 'type': 'array', 'uniqueItems': True, 'items': {'type': 'integer'}}}, 'required': ['a'], } @@ -295,13 +269,7 @@ class Model(BaseModel): 'items': [ {'type': 'string'}, {'type': 'integer'}, - { - 'anyOf': [ - {'type': 'string'}, - {'type': 'integer'}, - {'type': 'number'}, - ] - }, + {'anyOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'number'}]}, {'type': 'number'}, ], } @@ -339,11 +307,7 @@ class Model(BaseModel): 'properties': { 'a': {'title': 'A', 'anyOf': [{'type': 'integer'}, {'type': 'string'}]}, 'b': {'title': 'B', 'type': 'array', 'items': {'type': 'integer'}}, - 'c': { - 'title': 'C', - 'type': 'object', - 'additionalProperties': {'$ref': '#/definitions/Foo'}, - }, + 'c': {'title': 'C', 'type': 'object', 'additionalProperties': {'$ref': '#/definitions/Foo'}}, 'd': {'$ref': '#/definitions/Foo'}, 'e': {'title': 'E', 'type': 'object'}, }, @@ -386,14 +350,8 @@ class Model(BaseModel): 'properties': { 'a': {'title': 'A', 'type': 'string'}, 'b': {'title': 'B', 'type': 'string', 'format': 'binary'}, - 'c': { - 'title': 'C', - 'anyOf': [{'type': 'string'}, {'type': 'string', 'format': 'binary'}], - }, - 'd': { - 'title': 'D', - 'anyOf': [{'type': 'string'}, {'type': 'string', 'format': 'binary'}], - }, + 'c': {'title': 'C', 'anyOf': [{'type': 'string'}, {'type': 'string', 'format': 'binary'}]}, + 'd': {'title': 'D', 'anyOf': [{'type': 'string'}, {'type': 'string', 'format': 'binary'}]}, }, 'required': ['c'], } @@ -412,13 +370,7 @@ class Model(BaseModel): 'properties': { 'a': {'title': 'A', 'type': 'string'}, 'b': {'title': 'B', 'type': 'string'}, - 'c': { - 'title': 'C', - 'type': 'string', - 'minLength': 3, - 'maxLength': 5, - 'pattern': '^text$', - }, + 'c': {'title': 'C', 'type': 'string', 'minLength': 3, 'maxLength': 5, 'pattern': '^text$'}, }, 'required': ['a', 'b', 'c'], } @@ -438,20 +390,8 @@ class Model(BaseModel): 'type': 'object', 'properties': { 'a': {'title': 'A', 'type': 'string', 'format': 'email'}, - 'b': { - 'title': 'B', - 'type': 'string', - 'format': 'uri', - 'minLength': 1, - 'maxLength': 2 ** 16, - }, - 'c': { - 'title': 'C', - 'type': 'string', - 'format': 'uri', - 'minLength': 5, - 'maxLength': 10, - }, + 'b': {'title': 'B', 'type': 'string', 'format': 'uri', 'minLength': 1, 'maxLength': 2 ** 16}, + 'c': {'title': 'C', 'type': 'string', 'format': 'uri', 'minLength': 5, 'maxLength': 10}, 'd': {'title': 'D', 'type': 'string', 'format': 'name-email'}, 'e': {'title': 'E', 'type': 'string', 'format': 'dsn'}, }, @@ -473,12 +413,7 @@ class Model(BaseModel): 'type': 'object', 'properties': { 'a': {'title': 'A', 'type': 'integer'}, - 'b': { - 'title': 'B', - 'type': 'integer', - 'exclusiveMinimum': 5, - 'exclusiveMaximum': 10, - }, + 'b': {'title': 'B', 'type': 'integer', 'exclusiveMinimum': 5, 'exclusiveMaximum': 10}, 'c': {'title': 'C', 'type': 'integer', 'minimum': 5, 'maximum': 10}, 'd': {'title': 'D', 'type': 'integer', 'exclusiveMinimum': 0}, 'e': {'title': 'E', 'type': 'integer', 'exclusiveMaximum': 0}, @@ -504,22 +439,12 @@ class Model(BaseModel): 'type': 'object', 'properties': { 'a': {'title': 'A', 'type': 'number'}, - 'b': { - 'title': 'B', - 'type': 'number', - 'exclusiveMinimum': 5, - 'exclusiveMaximum': 10, - }, + 'b': {'title': 'B', 'type': 'number', 'exclusiveMinimum': 5, 'exclusiveMaximum': 10}, 'c': {'title': 'C', 'type': 'number', 'minimum': 5, 'maximum': 10}, 'd': {'title': 'D', 'type': 'number', 'exclusiveMinimum': 0}, 'e': {'title': 'E', 'type': 'number', 'exclusiveMaximum': 0}, 'f': {'title': 'F', 'type': 'number'}, - 'g': { - 'title': 'G', - 'type': 'number', - 'exclusiveMinimum': 5, - 'exclusiveMaximum': 10, - }, + 'g': {'title': 'G', 'type': 'number', 'exclusiveMinimum': 5, 'exclusiveMaximum': 10}, 'h': {'title': 'H', 'type': 'number', 'minimum': 5, 'maximum': 10}, }, 'required': ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], @@ -680,11 +605,7 @@ class Model(BaseModel): } }, }, - 'Baz': { - 'title': 'Baz', - 'type': 'object', - 'properties': {'c': {'$ref': '#/definitions/Bar'}}, - }, + 'Baz': {'title': 'Baz', 'type': 'object', 'properties': {'c': {'$ref': '#/definitions/Bar'}}}, }, 'properties': {'d': {'$ref': '#/definitions/Baz'}}, 'required': ['d'], @@ -712,9 +633,7 @@ class Pizza(BaseModel): ingredients: List[Ingredient] model_schema = schema( - [Model, Pizza], - title='Multi-model schema', - description='Single JSON Schema with multiple definitions', + [Model, Pizza], title='Multi-model schema', description='Single JSON Schema with multiple definitions' ) assert model_schema == { 'title': 'Multi-model schema', From c7f1d020eec63e300a69ebe2064147e93165aa7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 13:17:58 +0400 Subject: [PATCH 23/65] Fix test for schemas with email validation --- tests/test_schema.py | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/tests/test_schema.py b/tests/test_schema.py index 1aed96b619..5a45715d94 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -45,6 +45,11 @@ from .schema_test_package.modulec.modelc import Model as ModelC from .schema_test_package.moduled.modeld import Model as ModelD +try: + import email_validator +except ImportError: + email_validator = None + def test_key(): class ApplePie(BaseModel): @@ -377,12 +382,29 @@ class Model(BaseModel): def test_special_str_types(): + class Model(BaseModel): + a: UrlStr + b: urlstr(min_length=5, max_length=10) + c: DSN + + model_schema = Model.schema() + assert model_schema == { + 'title': 'Model', + 'type': 'object', + 'properties': { + 'a': {'title': 'A', 'type': 'string', 'format': 'uri', 'minLength': 1, 'maxLength': 2 ** 16}, + 'b': {'title': 'B', 'type': 'string', 'format': 'uri', 'minLength': 5, 'maxLength': 10}, + 'c': {'title': 'C', 'type': 'string', 'format': 'dsn'}, + }, + 'required': ['a', 'b', 'c'], + } + + +@pytest.mark.skipif(not email_validator, reason='email_validator not installed') +def test_email_str_types(): class Model(BaseModel): a: EmailStr - b: UrlStr - c: urlstr(min_length=5, max_length=10) - d: NameEmail - e: DSN + b: NameEmail model_schema = Model.schema() assert model_schema == { @@ -390,12 +412,9 @@ class Model(BaseModel): 'type': 'object', 'properties': { 'a': {'title': 'A', 'type': 'string', 'format': 'email'}, - 'b': {'title': 'B', 'type': 'string', 'format': 'uri', 'minLength': 1, 'maxLength': 2 ** 16}, - 'c': {'title': 'C', 'type': 'string', 'format': 'uri', 'minLength': 5, 'maxLength': 10}, - 'd': {'title': 'D', 'type': 'string', 'format': 'name-email'}, - 'e': {'title': 'E', 'type': 'string', 'format': 'dsn'}, + 'b': {'title': 'B', 'type': 'string', 'format': 'name-email'}, }, - 'required': ['a', 'b', 'c', 'd', 'e'], + 'required': ['a', 'b'], } From be5b349b84280fca563f8551f73cb20a184a08ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 13:36:15 +0400 Subject: [PATCH 24/65] Check if field is class before checking if is subclass --- pydantic/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index b14fe9e39d..65c57acd5b 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -25,7 +25,7 @@ def get_flat_models_from_field(field: Field) -> Set[Type['main.BaseModel']]: flat_models: Set[Type[main.BaseModel]] = set() if field.sub_fields: flat_models = flat_models | get_flat_models_from_sub_fields(field.sub_fields) - elif issubclass(field.type_, main.BaseModel): + elif isinstance(field.type_, type) and issubclass(field.type_, main.BaseModel): flat_models = flat_models | get_flat_models_from_model(field.type_) return flat_models From b3953e9f318f9a5a8ef1430ccca8cc7c7cef9088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 13:48:20 +0400 Subject: [PATCH 25/65] Improve schema error when using unsuported types --- pydantic/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index 65c57acd5b..e01c5ddc04 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -350,7 +350,7 @@ def field_singleton_schema( # noqa: C901 (ignore complexity) return {'$ref': f'{ref_prefix}{model_name}'}, definitions else: return sub_schema, definitions - raise ValueError('Value not declarable with JSON Schema') + raise ValueError(f'Value not declarable with JSON Schema, field: {field}') def model_schema(class_: 'main.BaseModel', by_alias=True, ref_prefix='#/definitions/') -> Dict[str, Any]: From 1fb23cc225d06486b5e28da88dece555475577f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 13:49:08 +0400 Subject: [PATCH 26/65] Add additional tests for corner cases, raise coverage to 100% --- tests/test_schema.py | 43 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/tests/test_schema.py b/tests/test_schema.py index 5a45715d94..0ecf66596f 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -1,6 +1,7 @@ from datetime import date, datetime, time, timedelta from decimal import Decimal from enum import Enum, IntEnum +from pathlib import Path from typing import Any, Dict, List, Optional, Set, Tuple, Union from uuid import UUID @@ -30,6 +31,7 @@ NoneStrBytes, PositiveFloat, PositiveInt, + PyObject, StrBytes, StrictStr, UrlStr, @@ -263,6 +265,7 @@ class Model(BaseModel): def test_tuple(): class Model(BaseModel): a: Tuple[str, int, Union[str, int, float], float] + b: Tuple[str] assert Model.schema() == { 'title': 'Model', @@ -277,8 +280,25 @@ class Model(BaseModel): {'anyOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'number'}]}, {'type': 'number'}, ], + }, + 'b': { + 'title': 'B', + 'type': 'array', + 'items': {'type': 'string'}, } }, + 'required': ['a', 'b'], + } + + +def test_bool(): + class Model(BaseModel): + a: bool + + assert Model.schema() == { + 'title': 'Model', + 'type': 'object', + 'properties': {'a': {'title': 'A', 'type': 'boolean'}}, 'required': ['a'], } @@ -497,6 +517,7 @@ def test_path_types(): class Model(BaseModel): a: FilePath b: DirectoryPath + c: Path model_schema = Model.schema() assert model_schema == { @@ -505,8 +526,9 @@ class Model(BaseModel): 'properties': { 'a': {'title': 'A', 'type': 'string', 'format': 'file-path'}, 'b': {'title': 'B', 'type': 'string', 'format': 'directory-path'}, + 'c': {'title': 'C', 'type': 'string', 'format': 'path'}, }, - 'required': ['a', 'b'], + 'required': ['a', 'b', 'c'], } @@ -523,6 +545,14 @@ class Model(BaseModel): } +def test_error_non_supported_types(): + class Model(BaseModel): + a: PyObject + + with pytest.raises(ValueError) as e: + Model.schema() + + def test_flat_models_unique_models(): flat_models = get_flat_models_from_models([ModelA, ModelB, ModelC]) assert flat_models == set([ModelA, ModelB]) @@ -717,13 +747,9 @@ class Baz(BaseModel): model_schema = schema( [Bar, Baz], - title='Multi-model schema', - description='Custom prefix for $ref fields', ref_prefix='#/components/schemas/', # OpenAPI style ) assert model_schema == { - 'title': 'Multi-model schema', - 'description': 'Custom prefix for $ref fields', 'definitions': { 'Baz': { 'title': 'Baz', @@ -745,3 +771,10 @@ class Baz(BaseModel): }, }, } + + +def test_schema_no_definitions(): + model_schema = schema([], title='Schema without definitions') + assert model_schema == { + 'title': 'Schema without definitions', + } From de2b0965d24e717a68f6d891520bce18696d587f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 13:50:40 +0400 Subject: [PATCH 27/65] Rename BaseModel.schema_json to schema_str (EAFP Python style) --- docs/examples/schema1.py | 2 +- pydantic/main.py | 2 +- tests/test_schema.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/examples/schema1.py b/docs/examples/schema1.py index a95252ae3d..a81c348da5 100644 --- a/docs/examples/schema1.py +++ b/docs/examples/schema1.py @@ -37,4 +37,4 @@ class Config: # 'properties': { # 'foo_bar': { # ... -print(MainModel.schema_json(indent=2)) +print(MainModel.schema_str(indent=2)) diff --git a/pydantic/main.py b/pydantic/main.py index 621bfa5f63..2b41e42d75 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -327,7 +327,7 @@ def schema(cls, by_alias=True) -> Dict[str, Any]: return s @classmethod - def schema_json(cls, *, by_alias=True, **dumps_kwargs) -> str: + def schema_str(cls, *, by_alias=True, **dumps_kwargs) -> str: from .json import pydantic_encoder return json.dumps(cls.schema(by_alias=by_alias), default=pydantic_encoder, **dumps_kwargs) diff --git a/tests/test_schema.py b/tests/test_schema.py index 0ecf66596f..ed7b7e7a29 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -187,7 +187,7 @@ class Model(BaseModel): a = b'foobar' b = Decimal('12.34') - assert Model.schema_json(indent=2) == ( + assert Model.schema_str(indent=2) == ( '{\n' ' "title": "Model",\n' ' "type": "object",\n' From 409eb0c3a4149facd452da81c9523e53e6e13c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 14:02:50 +0400 Subject: [PATCH 28/65] Add more tests to utils.display_as_type to increase the coverage for enums --- tests/test_utils.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index 44f6399ea1..344f5e02ae 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,4 +1,5 @@ import os +from enum import Enum from typing import Union import pytest @@ -107,3 +108,27 @@ def test_import_no_attr(): ) def test_display_as_type(value, expected): assert display_as_type(value) == expected + + +def test_display_as_type_enum(): + class SubField(Enum): + a = 1 + b = 'b' + displayed = display_as_type(SubField) + assert displayed == 'enum' + + +def test_display_as_type_enum_int(): + class SubField(int, Enum): + a = 1 + b = 2 + displayed = display_as_type(SubField) + assert displayed == 'int' + + +def test_display_as_type_enum_str(): + class SubField(str, Enum): + a = 'a' + b = 'b' + displayed = display_as_type(SubField) + assert displayed == 'str' From e306f4ec1e5dee3cac58436a5d9438dedd6d669b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 14:04:17 +0400 Subject: [PATCH 29/65] Remove unused catched error in schema tests --- tests/test_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_schema.py b/tests/test_schema.py index ed7b7e7a29..0d32d872ed 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -549,7 +549,7 @@ def test_error_non_supported_types(): class Model(BaseModel): a: PyObject - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError): Model.schema() From 1536c7e7db59c8b36d8d3e97ce6df3016d4298c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 14:06:41 +0400 Subject: [PATCH 30/65] Fix formatting with black --- tests/test_schema.py | 17 ++++------------- tests/test_utils.py | 3 +++ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/tests/test_schema.py b/tests/test_schema.py index 0d32d872ed..85cf95f4c8 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -281,11 +281,7 @@ class Model(BaseModel): {'type': 'number'}, ], }, - 'b': { - 'title': 'B', - 'type': 'array', - 'items': {'type': 'string'}, - } + 'b': {'title': 'B', 'type': 'array', 'items': {'type': 'string'}}, }, 'required': ['a', 'b'], } @@ -745,10 +741,7 @@ class Bar(BaseModel): class Baz(BaseModel): c: Bar - model_schema = schema( - [Bar, Baz], - ref_prefix='#/components/schemas/', # OpenAPI style - ) + model_schema = schema([Bar, Baz], ref_prefix='#/components/schemas/') # OpenAPI style assert model_schema == { 'definitions': { 'Baz': { @@ -769,12 +762,10 @@ class Baz(BaseModel): 'properties': {'a': {'title': 'A', 'type': 'string'}}, 'required': ['a'], }, - }, + } } def test_schema_no_definitions(): model_schema = schema([], title='Schema without definitions') - assert model_schema == { - 'title': 'Schema without definitions', - } + assert model_schema == {'title': 'Schema without definitions'} diff --git a/tests/test_utils.py b/tests/test_utils.py index 344f5e02ae..276e723fd0 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -114,6 +114,7 @@ def test_display_as_type_enum(): class SubField(Enum): a = 1 b = 'b' + displayed = display_as_type(SubField) assert displayed == 'enum' @@ -122,6 +123,7 @@ def test_display_as_type_enum_int(): class SubField(int, Enum): a = 1 b = 2 + displayed = display_as_type(SubField) assert displayed == 'int' @@ -130,5 +132,6 @@ def test_display_as_type_enum_str(): class SubField(str, Enum): a = 'a' b = 'b' + displayed = display_as_type(SubField) assert displayed == 'str' From da1e805e1e6adef192cb73d38cd127630499f812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 19:09:16 +0400 Subject: [PATCH 31/65] Update docs schema example --- docs/examples/schema1.json | 54 +++++++++++++++++++++----------------- docs/examples/schema1.py | 15 +++++------ 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/docs/examples/schema1.json b/docs/examples/schema1.json index e9c2b04fc6..6ca0a928c7 100644 --- a/docs/examples/schema1.json +++ b/docs/examples/schema1.json @@ -1,42 +1,48 @@ { - "type": "object", "title": "Main", "description": "This is the description of the main model", + "type": "object", "properties": { "foo_bar": { - "type": "object", + "$ref": "#/definitions/FooBar" + }, + "Gender": { + "title": "Gender", + "enum": [ + "male", + "female", + "other", + "not_given" + ], + "type": "string" + }, + "snap": { + "title": "The Snap", + "default": 42, + "description": "this is the value of snap", + "type": "integer" + } + }, + "required": [ + "foo_bar" + ], + "definitions": { + "FooBar": { "title": "FooBar", + "type": "object", "properties": { "count": { - "type": "int", "title": "Count", - "required": true + "type": "integer" }, "size": { - "type": "float", "title": "Size", - "required": false + "type": "number" } }, - "required": true - }, - "Gender": { - "type": "int", - "title": "Gender", - "required": false, - "choices": [ - [1, "Male"], - [2, "Female"], - [3, "Other"], - [4, "I'd rather not say"] + "required": [ + "count" ] - }, - "snap": { - "type": "int", - "title": "The Snap", - "required": false, - "default": 42, - "description": "this is the value of snap" } } } diff --git a/docs/examples/schema1.py b/docs/examples/schema1.py index a81c348da5..72969a221f 100644 --- a/docs/examples/schema1.py +++ b/docs/examples/schema1.py @@ -1,15 +1,15 @@ -from enum import IntEnum +from enum import Enum from pydantic import BaseModel, Schema class FooBar(BaseModel): count: int size: float = None -class Gender(IntEnum): - male = 1 - female = 2 - other = 3 - not_given = 4 +class Gender(str, Enum): + male = 'male' + female = 'female' + other = 'other' + not_given = 'not_given' class MainModel(BaseModel): """ @@ -19,12 +19,11 @@ class MainModel(BaseModel): gender: Gender = Schema( None, alias='Gender', - choice_names={3: 'Other Gender', 4: "I'd rather not say"} ) snap: int = Schema( 42, title='The Snap', - description='this is the value of snap' + description='this is the value of snap', ) class Config: From 0671f6a5b1ce3a95c109e6584a006418e8529bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 19:09:42 +0400 Subject: [PATCH 32/65] Add schema examples for top-level schema with multiple models --- docs/examples/schema2.json | 40 ++++++++++++++++++++++++++++++++++++++ docs/examples/schema2.py | 26 +++++++++++++++++++++++++ docs/examples/schema3.json | 29 +++++++++++++++++++++++++++ docs/examples/schema3.py | 20 +++++++++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 docs/examples/schema2.json create mode 100644 docs/examples/schema2.py create mode 100644 docs/examples/schema3.json create mode 100644 docs/examples/schema3.py diff --git a/docs/examples/schema2.json b/docs/examples/schema2.json new file mode 100644 index 0000000000..8de2dfbee7 --- /dev/null +++ b/docs/examples/schema2.json @@ -0,0 +1,40 @@ +{ + "title": "My Schema", + "definitions": { + "Foo": { + "title": "Foo", + "type": "object", + "properties": { + "a": { + "title": "A", + "type": "string" + } + } + }, + "Model": { + "title": "Model", + "type": "object", + "properties": { + "b": { + "$ref": "#/definitions/Foo" + } + }, + "required": [ + "b" + ] + }, + "Bar": { + "title": "Bar", + "type": "object", + "properties": { + "c": { + "title": "C", + "type": "integer" + } + }, + "required": [ + "c" + ] + } + } +} diff --git a/docs/examples/schema2.py b/docs/examples/schema2.py new file mode 100644 index 0000000000..ef867ab4fb --- /dev/null +++ b/docs/examples/schema2.py @@ -0,0 +1,26 @@ +import json +from pydantic import BaseModel +from pydantic.schema import schema + + +class Foo(BaseModel): + a: str = None + + +class Model(BaseModel): + b: Foo + + +class Bar(BaseModel): + c: int + + +top_level_schema = schema([Model, Bar], title='My Schema') +print(json.dumps(top_level_schema, indent=2)) + +# { +# "title": "My Schema", +# "definitions": { +# "Foo": { +# "title": "Foo", +# ... diff --git a/docs/examples/schema3.json b/docs/examples/schema3.json new file mode 100644 index 0000000000..aa238453c1 --- /dev/null +++ b/docs/examples/schema3.json @@ -0,0 +1,29 @@ +{ + "definitions": { + "Foo": { + "title": "Foo", + "type": "object", + "properties": { + "a": { + "title": "A", + "type": "integer" + } + }, + "required": [ + "a" + ] + }, + "Model": { + "title": "Model", + "type": "object", + "properties": { + "a": { + "$ref": "#/components/schemas/Foo" + } + }, + "required": [ + "a" + ] + } + } +} diff --git a/docs/examples/schema3.py b/docs/examples/schema3.py new file mode 100644 index 0000000000..5c3ee2e1b3 --- /dev/null +++ b/docs/examples/schema3.py @@ -0,0 +1,20 @@ +import json +from pydantic import BaseModel +from pydantic.schema import schema + +class Foo(BaseModel): + a: int + +class Model(BaseModel): + a: Foo + + +top_level_schema = schema([Model], ref_prefix='#/components/schemas/') # Default location for OpenAPI +print(json.dumps(top_level_schema, indent=2)) + +# { +# "definitions": { +# "Foo": { +# "title": "Foo", +# "type": "object", +# ... From 6eedbf869546d69dceb49ab1a9414073955e4f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 19:10:45 +0400 Subject: [PATCH 33/65] Update docs, section Schema, with new JSON Schema generation details --- docs/index.rst | 139 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 131 insertions(+), 8 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index b931a32a55..9f6b1516d8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -219,7 +219,7 @@ The ellipsis ``...`` just means "Required" same as annotation only declarations Schema Creation ............... -*Pydantic* allows auto creation of schemas from models: +*Pydantic* allows auto creation of JSON Schemas from models: .. literalinclude:: examples/schema1.py @@ -229,22 +229,23 @@ Outputs: (This script is complete, it should run "as is") -`schema` will return a dict of the schema, while `schema_json` will return a JSON representation of that. +The generated schemas are compliant with the specifications: `JSON Schema Core `_, `JSON Schema Validation `_, `OpenAPI `_. -"submodels" are recursively included in the schema. +``schema`` will return a dict of the schema, while ``schema_str`` will return a JSON string representation of that. -The ``description`` for models is taken from the docstring of the class. +"submodels" used are added to the ``definitons`` JSON attribute and referenced between them, as per the spec. + +All submodels (and their submodels) schemas are put directly in a top-level ``definitions`` JSON key for easy re-usability and reference. -Enums are shown in the schema as choices, optionally the ``choice_names`` argument can be used -to provide human friendly descriptions for the choices. If ``choice_names`` is omitted or misses values, -descriptions will be generated by calling ``.title()`` on the name of the member. +"submodels" with modifications via the (``Schema`` class) like a custom title, description or default value, are recursively included instead of referenced. + +The ``description`` for models is taken from the docstring of the class. Optionally the ``Schema`` class can be used to provide extra information about the field, arguments: * ``default`` (positional argument), since the ``Schema`` is replacing the field's default, its first argument is used to set the default, use ellipsis (``...``) to indicate the field is required * ``title`` if omitted ``field_name.title()`` is used -* ``choice_names`` as described above * ``alias`` - the public name of the field. * ``**`` any other keyword arguments (eg. ``description``) will be added verbatim to the field's schema @@ -254,6 +255,128 @@ to set all the arguments above except ``default``. The schema is generated by default using aliases as keys, it can also be generated using model property names not aliases with ``MainModel.schema/schema_json(by_alias=False)``. +Types, custom field types, and constraints (as `max_length`) are mapped to the corresponding `JSON Schema Core `_ spec format when there's an equivalent available, or to `JSON Schema Validation `_, `OpenAPI Data Types `_ (which are based on JSON Schema), or otherwise use the standard ``format`` JSON field to define Pydantic extensions for more complex ``string`` sub-types. + +The field schema mapping from Python / Pydantic to JSON Schema is done as follows: + + ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Python type | JSON Schema Type | Additional JSON Schema | Defined in | Notes | ++=============================================================+==================+==================================================================================+======================================+=================================================================================================================================================================================================================================+ +| ``bool`` | ``boolean`` | | JSON Schema Core | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``str`` | ``string`` | | JSON Schema Core | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``float`` | ``number`` | | JSON Schema Core | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``int`` | ``integer`` | | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``dict`` | ``object`` | | JSON Schema Core | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``list`` | ``array`` | | JSON Schema Core | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``tuple`` | ``array`` | | JSON Schema Core | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``set`` | ``array`` | ``{"uniqueItems": true}`` | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``List[str]`` | ``array`` | ``{"items": {"type": "string"}}`` | JSON Schema Validation | And equivalent for any other sub type, e.g. List[int] | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``Tuple[str, int]`` | ``array`` | ``{"items": [{"type": "string"}, {"type": "integer"}]}`` | JSON Schema Validation | And equivalent for any other set of subtypes. Note: If using schemas for OpenAPI, you shouldn't use this declaration, as it would not be valid in OpenAPI (although it is valid in JSON Schema) | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``Dict[str, int]`` | ``object`` | ``{"additionalProperties": {"type": "integer"}}`` | JSON Schema Validation | And equivalent for any other subfields for dicts. Have in mind that although you can use other types as keys for dicts with Pydantic, only strings are valid keys for JSON, and so, only str is valid as JSON Schema key types. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``Union[str, int]`` | * ``anyOf`` | ``{"anyOf": [{"type": "string"}, {"type": "integer"}]}`` | JSON Schema Validation | And equivalent for any other subfields for unions. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``Enum`` | * ``enum`` | ``{"enum": [...]}`` | JSON Schema Validation | All the literal values in the enum are included in the definition. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``EmailStr`` | ``string`` | ``{"format": "email"}`` | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``NameEmail`` | ``string`` | ``{"format": "name-email"}`` | Pydantic standard "format" extension | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``UrlStr`` | ``string`` | ``{"format": "uri"}`` | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``DSN`` | ``string`` | ``{"format": "dsn"}`` | Pydantic standard "format" extension | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``bytes`` | ``string`` | ``{"format": "binary"}`` | OpenAPI | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``Decimal`` | ``number`` | | JSON Schema Core | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``UUID1`` | ``string`` | ``{"format": "uuid1"}`` | Pydantic standard "format" extension | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``UUID3`` | ``string`` | ``{"format": "uuid3"}`` | Pydantic standard "format" extension | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``UUID4`` | ``string`` | ``{"format": "uuid4"}`` | Pydantic standard "format" extension | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``UUID5`` | ``string`` | ``{"format": "uuid5"}`` | Pydantic standard "format" extension | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``UUID`` | ``string`` | ``{"format": "uuid"}`` | Pydantic standard "format" extension | Suggested in OpenAPI | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``FilePath`` | ``string`` | ``{"format": "file-path"}`` | Pydantic standard "format" extension | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``DirectoryPath`` | ``string`` | ``{"format": "directory-path"}`` | Pydantic standard "format" extension | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``Path`` | ``string`` | ``{"format": "path"}`` | Pydantic standard "format" extension | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``datetime`` | ``string`` | ``{"format": "date-time"}`` | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``date`` | ``string`` | ``{"format": "date"}`` | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``time`` | ``string`` | ``{"format": "time"}`` | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``timedelta`` | ``string`` | ``{"format": "time-delta"}`` | Pydantic standard "format" extension | Suggested in JSON Schema repository's issues by maintainer | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``Json`` | ``string`` | ``{"format": "json-string"}`` | Pydantic standard "format" extension | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``StrictStr`` | ``string`` | | JSON Schema Core | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``ConstrainedStr`` | ``string`` | | JSON Schema Core | If the type has values declared for the constraints, they are included as validations. See the mapping for ``constr`` below. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``constr(regex='^text$', min_length=2, max_length=10)`` | ``string`` | ``{"pattern": "^text$", "minLength": 2, "maxLength": 10}`` | JSON Schema Validation | Any argument not passed to the function (not defined) will not be included in the schema. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``ConstrainedInt`` | ``integer`` | | JSON Schema Core | If the type has values declared for the constraints, they are included as validations. See the mapping for ``conint`` below. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``conint(gt=1, ge=2, lt=6, le=5)`` | ``integer`` | ``{"maximum": 5, "exclusiveMaximum": 6, "minimum": 2, "exclusiveMinimum": 1}`` | | Any argument not passed to the function (not defined) will not be included in the schema. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``PositiveInt`` | ``integer`` | ``{"exclusiveMinimum": 0}`` | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``NegativeInt`` | ``integer`` | ``{"exclusiveMaximum": 0}`` | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``ConstrainedFloat`` | ``number`` | | JSON Schema Core | If the type has values declared for the constraints, they are included as validations. See the mapping for ``confloat`` below. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``confloat(gt=1, ge=2, lt=6, le=5)`` | ``number`` | ``{"maximum": 5, "exclusiveMaximum": 6, "minimum": 2, "exclusiveMinimum": 1}`` | JSON Schema Validation | Any argument not passed to the function (not defined) will not be included in the schema. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``PositiveFloat`` | ``number`` | ``{"exclusiveMinimum": 0}`` | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``NegativeFloat`` | ``number`` | ``{"exclusiveMaximum": 0}`` | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``ConstrainedDecimal`` | ``number`` | | JSON Schema Core | If the type has values declared for the constraints, they are included as validations. See the mapping for ``condecimal`` below. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``condecimal(gt=1, ge=2, lt=6, le=5)`` | ``number`` | ``{"maximum": 5, "exclusiveMaximum": 6, "minimum": 2, "exclusiveMinimum": 1}`` | JSON Schema Validation | Any argument not passed to the function (not defined) will not be included in the schema. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``BaseModel`` | ``object`` | | JSON Schema Core | All the properties defined will be defined with standard JSON Schema, including submodels | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + +You can also generate a top-level JSON Schema that only includes a list of models and all their related submodules: + +.. literalinclude:: examples/schema2.py + +Outputs: + +.. literalinclude:: examples/schema2.json + +(This script is complete, it should run "as is") + +You can customize the generated ``$ref`` JSON location, the definitions will still be in the key ``definitions`` and you can still get them from there, but the references will point to your defined prefix instead of the default. This is useful if you need to extend or modify JSON Schema default definitions location, e.g. with OpenAPI: + +.. literalinclude:: examples/schema3.py + +Outputs: + +.. literalinclude:: examples/schema3.json + +(This script is complete, it should run "as is") + + Error Handling .............. From f63084fb8b16012edaa949718d3d7a98582096fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 19:11:20 +0400 Subject: [PATCH 34/65] Update docs, history, with new features --- HISTORY.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 7883989c5f..d48b2e5e28 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,12 @@ History ------- +v0.16.0 (2018-11-19) +.................. + +* refactor schema generation to be compatible with JSON Schema and OpenAPI specs, #308 by @tiangolo +* add ``schema`` to ``schema`` module to generate top-level schemas from base models, #308 by @tiangolo + v0.15.0 (2018-11-18) .................... * move codebase to use black, #287 by @samuelcolvin From f88624aa6687ae994489f46cd1166825ae87bf96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 19:11:47 +0400 Subject: [PATCH 35/65] Update fields, remove unnecessary schema code for enums --- pydantic/fields.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pydantic/fields.py b/pydantic/fields.py index 670bb5a647..e8fe4fb712 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -39,13 +39,12 @@ class Schema: Used to provide extra information about a field in a model schema. """ - __slots__ = 'default', 'alias', 'title', 'choice_names', 'extra' + __slots__ = 'default', 'alias', 'title', 'extra' - def __init__(self, default, *, alias=None, title=None, choice_names=None, **extra): + def __init__(self, default, *, alias=None, title=None, **extra): self.default = default self.alias = alias self.title = title - self.choice_names = choice_names self.extra = extra From b368fa0fc386902c6dd211214d9d077c38440c9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 19:28:09 +0400 Subject: [PATCH 36/65] Update docs, fix links and typos in Schema section --- docs/index.rst | 209 +++++++++++++++++++++++++------------------------ 1 file changed, 105 insertions(+), 104 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 9f6b1516d8..733e90aaaa 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -229,13 +229,13 @@ Outputs: (This script is complete, it should run "as is") -The generated schemas are compliant with the specifications: `JSON Schema Core `_, `JSON Schema Validation `_, `OpenAPI `_. +The generated schemas are compliant with the specifications: `JSON Schema Core `__, `JSON Schema Validation `__ and `OpenAPI `__. -``schema`` will return a dict of the schema, while ``schema_str`` will return a JSON string representation of that. +``BaseModel.schema`` will return a dict of the schema, while ``BaseModel.schema_str`` will return a JSON string representation of that. -"submodels" used are added to the ``definitons`` JSON attribute and referenced between them, as per the spec. +"submodels" used are added to the ``definitons`` JSON attribute and referenced, as per the spec. -All submodels (and their submodels) schemas are put directly in a top-level ``definitions`` JSON key for easy re-usability and reference. +All submodels (and their submodels) schemas are put directly in a top-level ``definitions`` JSON key for easy re-use and reference. "submodels" with modifications via the (``Schema`` class) like a custom title, description or default value, are recursively included instead of referenced. @@ -255,108 +255,107 @@ to set all the arguments above except ``default``. The schema is generated by default using aliases as keys, it can also be generated using model property names not aliases with ``MainModel.schema/schema_json(by_alias=False)``. -Types, custom field types, and constraints (as `max_length`) are mapped to the corresponding `JSON Schema Core `_ spec format when there's an equivalent available, or to `JSON Schema Validation `_, `OpenAPI Data Types `_ (which are based on JSON Schema), or otherwise use the standard ``format`` JSON field to define Pydantic extensions for more complex ``string`` sub-types. +Types, custom field types, and constraints (as ``max_length``) are mapped to the corresponding `JSON Schema Core `__ spec format when there's an equivalent available, next to `JSON Schema Validation `__, `OpenAPI Data Types `__ (which are based on JSON Schema), or otherwise use the standard ``format`` JSON field to define Pydantic extensions for more complex ``string`` sub-types. The field schema mapping from Python / Pydantic to JSON Schema is done as follows: - -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| Python type | JSON Schema Type | Additional JSON Schema | Defined in | Notes | -+=============================================================+==================+==================================================================================+======================================+=================================================================================================================================================================================================================================+ -| ``bool`` | ``boolean`` | | JSON Schema Core | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``str`` | ``string`` | | JSON Schema Core | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``float`` | ``number`` | | JSON Schema Core | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``int`` | ``integer`` | | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``dict`` | ``object`` | | JSON Schema Core | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``list`` | ``array`` | | JSON Schema Core | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``tuple`` | ``array`` | | JSON Schema Core | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``set`` | ``array`` | ``{"uniqueItems": true}`` | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``List[str]`` | ``array`` | ``{"items": {"type": "string"}}`` | JSON Schema Validation | And equivalent for any other sub type, e.g. List[int] | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``Tuple[str, int]`` | ``array`` | ``{"items": [{"type": "string"}, {"type": "integer"}]}`` | JSON Schema Validation | And equivalent for any other set of subtypes. Note: If using schemas for OpenAPI, you shouldn't use this declaration, as it would not be valid in OpenAPI (although it is valid in JSON Schema) | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``Dict[str, int]`` | ``object`` | ``{"additionalProperties": {"type": "integer"}}`` | JSON Schema Validation | And equivalent for any other subfields for dicts. Have in mind that although you can use other types as keys for dicts with Pydantic, only strings are valid keys for JSON, and so, only str is valid as JSON Schema key types. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``Union[str, int]`` | * ``anyOf`` | ``{"anyOf": [{"type": "string"}, {"type": "integer"}]}`` | JSON Schema Validation | And equivalent for any other subfields for unions. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``Enum`` | * ``enum`` | ``{"enum": [...]}`` | JSON Schema Validation | All the literal values in the enum are included in the definition. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``EmailStr`` | ``string`` | ``{"format": "email"}`` | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``NameEmail`` | ``string`` | ``{"format": "name-email"}`` | Pydantic standard "format" extension | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``UrlStr`` | ``string`` | ``{"format": "uri"}`` | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``DSN`` | ``string`` | ``{"format": "dsn"}`` | Pydantic standard "format" extension | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``bytes`` | ``string`` | ``{"format": "binary"}`` | OpenAPI | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``Decimal`` | ``number`` | | JSON Schema Core | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``UUID1`` | ``string`` | ``{"format": "uuid1"}`` | Pydantic standard "format" extension | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``UUID3`` | ``string`` | ``{"format": "uuid3"}`` | Pydantic standard "format" extension | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``UUID4`` | ``string`` | ``{"format": "uuid4"}`` | Pydantic standard "format" extension | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``UUID5`` | ``string`` | ``{"format": "uuid5"}`` | Pydantic standard "format" extension | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``UUID`` | ``string`` | ``{"format": "uuid"}`` | Pydantic standard "format" extension | Suggested in OpenAPI | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``FilePath`` | ``string`` | ``{"format": "file-path"}`` | Pydantic standard "format" extension | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``DirectoryPath`` | ``string`` | ``{"format": "directory-path"}`` | Pydantic standard "format" extension | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``Path`` | ``string`` | ``{"format": "path"}`` | Pydantic standard "format" extension | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``datetime`` | ``string`` | ``{"format": "date-time"}`` | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``date`` | ``string`` | ``{"format": "date"}`` | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``time`` | ``string`` | ``{"format": "time"}`` | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``timedelta`` | ``string`` | ``{"format": "time-delta"}`` | Pydantic standard "format" extension | Suggested in JSON Schema repository's issues by maintainer | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``Json`` | ``string`` | ``{"format": "json-string"}`` | Pydantic standard "format" extension | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``StrictStr`` | ``string`` | | JSON Schema Core | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``ConstrainedStr`` | ``string`` | | JSON Schema Core | If the type has values declared for the constraints, they are included as validations. See the mapping for ``constr`` below. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``constr(regex='^text$', min_length=2, max_length=10)`` | ``string`` | ``{"pattern": "^text$", "minLength": 2, "maxLength": 10}`` | JSON Schema Validation | Any argument not passed to the function (not defined) will not be included in the schema. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``ConstrainedInt`` | ``integer`` | | JSON Schema Core | If the type has values declared for the constraints, they are included as validations. See the mapping for ``conint`` below. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``conint(gt=1, ge=2, lt=6, le=5)`` | ``integer`` | ``{"maximum": 5, "exclusiveMaximum": 6, "minimum": 2, "exclusiveMinimum": 1}`` | | Any argument not passed to the function (not defined) will not be included in the schema. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``PositiveInt`` | ``integer`` | ``{"exclusiveMinimum": 0}`` | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``NegativeInt`` | ``integer`` | ``{"exclusiveMaximum": 0}`` | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``ConstrainedFloat`` | ``number`` | | JSON Schema Core | If the type has values declared for the constraints, they are included as validations. See the mapping for ``confloat`` below. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``confloat(gt=1, ge=2, lt=6, le=5)`` | ``number`` | ``{"maximum": 5, "exclusiveMaximum": 6, "minimum": 2, "exclusiveMinimum": 1}`` | JSON Schema Validation | Any argument not passed to the function (not defined) will not be included in the schema. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``PositiveFloat`` | ``number`` | ``{"exclusiveMinimum": 0}`` | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``NegativeFloat`` | ``number`` | ``{"exclusiveMaximum": 0}`` | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``ConstrainedDecimal`` | ``number`` | | JSON Schema Core | If the type has values declared for the constraints, they are included as validations. See the mapping for ``condecimal`` below. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``condecimal(gt=1, ge=2, lt=6, le=5)`` | ``number`` | ``{"maximum": 5, "exclusiveMaximum": 6, "minimum": 2, "exclusiveMinimum": 1}`` | JSON Schema Validation | Any argument not passed to the function (not defined) will not be included in the schema. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``BaseModel`` | ``object`` | | JSON Schema Core | All the properties defined will be defined with standard JSON Schema, including submodels | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - -You can also generate a top-level JSON Schema that only includes a list of models and all their related submodules: ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Python type | JSON Schema Type | Additional JSON Schema | Defined in | Notes | ++=============================================================+==================+==================================================================================+======================================+===================================================================================================================================================================================================================================+ +| ``bool`` | ``boolean`` | | JSON Schema Core | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``str`` | ``string`` | | JSON Schema Core | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``float`` | ``number`` | | JSON Schema Core | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``int`` | ``integer`` | | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``dict`` | ``object`` | | JSON Schema Core | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``list`` | ``array`` | | JSON Schema Core | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``tuple`` | ``array`` | | JSON Schema Core | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``set`` | ``array`` | ``{"uniqueItems": true}`` | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``List[str]`` | ``array`` | ``{"items": {"type": "string"}}`` | JSON Schema Validation | And equivalently for any other sub type, e.g. List[int]. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``Tuple[str, int]`` | ``array`` | ``{"items": [{"type": "string"}, {"type": "integer"}]}`` | JSON Schema Validation | And equivalently for any other set of subtypes. Note: If using schemas for OpenAPI, you shouldn't use this declaration, as it would not be valid in OpenAPI (although it is valid in JSON Schema). | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``Dict[str, int]`` | ``object`` | ``{"additionalProperties": {"type": "integer"}}`` | JSON Schema Validation | And equivalently for any other subfields for dicts. Have in mind that although you can use other types as keys for dicts with Pydantic, only strings are valid keys for JSON, and so, only str is valid as JSON Schema key types. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``Union[str, int]`` | * ``anyOf`` | ``{"anyOf": [{"type": "string"}, {"type": "integer"}]}`` | JSON Schema Validation | And equivalently for any other subfields for unions. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``Enum`` | * ``enum`` | ``{"enum": [...]}`` | JSON Schema Validation | All the literal values in the enum are included in the definition. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``EmailStr`` | ``string`` | ``{"format": "email"}`` | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``NameEmail`` | ``string`` | ``{"format": "name-email"}`` | Pydantic standard "format" extension | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``UrlStr`` | ``string`` | ``{"format": "uri"}`` | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``DSN`` | ``string`` | ``{"format": "dsn"}`` | Pydantic standard "format" extension | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``bytes`` | ``string`` | ``{"format": "binary"}`` | OpenAPI | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``Decimal`` | ``number`` | | JSON Schema Core | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``UUID1`` | ``string`` | ``{"format": "uuid1"}`` | Pydantic standard "format" extension | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``UUID3`` | ``string`` | ``{"format": "uuid3"}`` | Pydantic standard "format" extension | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``UUID4`` | ``string`` | ``{"format": "uuid4"}`` | Pydantic standard "format" extension | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``UUID5`` | ``string`` | ``{"format": "uuid5"}`` | Pydantic standard "format" extension | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``UUID`` | ``string`` | ``{"format": "uuid"}`` | Pydantic standard "format" extension | Suggested in OpenAPI. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``FilePath`` | ``string`` | ``{"format": "file-path"}`` | Pydantic standard "format" extension | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``DirectoryPath`` | ``string`` | ``{"format": "directory-path"}`` | Pydantic standard "format" extension | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``Path`` | ``string`` | ``{"format": "path"}`` | Pydantic standard "format" extension | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``datetime`` | ``string`` | ``{"format": "date-time"}`` | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``date`` | ``string`` | ``{"format": "date"}`` | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``time`` | ``string`` | ``{"format": "time"}`` | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``timedelta`` | ``string`` | ``{"format": "time-delta"}`` | Pydantic standard "format" extension | Suggested in JSON Schema repository's issues by maintainer. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``Json`` | ``string`` | ``{"format": "json-string"}`` | Pydantic standard "format" extension | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``StrictStr`` | ``string`` | | JSON Schema Core | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``ConstrainedStr`` | ``string`` | | JSON Schema Core | If the type has values declared for the constraints, they are included as validations. See the mapping for ``constr`` below. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``constr(regex='^text$', min_length=2, max_length=10)`` | ``string`` | ``{"pattern": "^text$", "minLength": 2, "maxLength": 10}`` | JSON Schema Validation | Any argument not passed to the function (not defined) will not be included in the schema. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``ConstrainedInt`` | ``integer`` | | JSON Schema Core | If the type has values declared for the constraints, they are included as validations. See the mapping for ``conint`` below. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``conint(gt=1, ge=2, lt=6, le=5)`` | ``integer`` | ``{"maximum": 5, "exclusiveMaximum": 6, "minimum": 2, "exclusiveMinimum": 1}`` | | Any argument not passed to the function (not defined) will not be included in the schema. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``PositiveInt`` | ``integer`` | ``{"exclusiveMinimum": 0}`` | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``NegativeInt`` | ``integer`` | ``{"exclusiveMaximum": 0}`` | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``ConstrainedFloat`` | ``number`` | | JSON Schema Core | If the type has values declared for the constraints, they are included as validations. See the mapping for ``confloat`` below. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``confloat(gt=1, ge=2, lt=6, le=5)`` | ``number`` | ``{"maximum": 5, "exclusiveMaximum": 6, "minimum": 2, "exclusiveMinimum": 1}`` | JSON Schema Validation | Any argument not passed to the function (not defined) will not be included in the schema. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``PositiveFloat`` | ``number`` | ``{"exclusiveMinimum": 0}`` | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``NegativeFloat`` | ``number`` | ``{"exclusiveMaximum": 0}`` | JSON Schema Validation | | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``ConstrainedDecimal`` | ``number`` | | JSON Schema Core | If the type has values declared for the constraints, they are included as validations. See the mapping for ``condecimal`` below. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``condecimal(gt=1, ge=2, lt=6, le=5)`` | ``number`` | ``{"maximum": 5, "exclusiveMaximum": 6, "minimum": 2, "exclusiveMinimum": 1}`` | JSON Schema Validation | Any argument not passed to the function (not defined) will not be included in the schema. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``BaseModel`` | ``object`` | | JSON Schema Core | All the properties defined will be defined with standard JSON Schema, including submodels. | ++-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + +You can also generate a top-level JSON Schema that only includes a list of models and all their related submodules in its ``definitions``: .. literalinclude:: examples/schema2.py @@ -366,7 +365,9 @@ Outputs: (This script is complete, it should run "as is") -You can customize the generated ``$ref`` JSON location, the definitions will still be in the key ``definitions`` and you can still get them from there, but the references will point to your defined prefix instead of the default. This is useful if you need to extend or modify JSON Schema default definitions location, e.g. with OpenAPI: +You can customize the generated ``$ref`` JSON location, the definitions will still be in the key ``definitions`` and you can still get them from there, but the references will point to your defined prefix instead of the default. + +This is useful if you need to extend or modify JSON Schema default definitions location, e.g. with OpenAPI: .. literalinclude:: examples/schema3.py From fb3b58df7b7b3c895e52e4d656298056a887dfcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 19:32:47 +0400 Subject: [PATCH 37/65] Trigger CI, as Python 3.7-dev seems to have random CI errors --- docs/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.rst b/docs/index.rst index 733e90aaaa..92d4a1fc51 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -355,6 +355,7 @@ The field schema mapping from Python / Pydantic to JSON Schema is done as follow | ``BaseModel`` | ``object`` | | JSON Schema Core | All the properties defined will be defined with standard JSON Schema, including submodels. | +-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + You can also generate a top-level JSON Schema that only includes a list of models and all their related submodules in its ``definitions``: .. literalinclude:: examples/schema2.py From b31057890fe3a22bee6e4524b821b502aa08b799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 19:37:04 +0400 Subject: [PATCH 38/65] Revert Model.schema_str to Model.schema_json as requested --- docs/examples/schema1.py | 2 +- docs/index.rst | 2 +- pydantic/main.py | 2 +- tests/test_schema.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/examples/schema1.py b/docs/examples/schema1.py index 72969a221f..a44f003476 100644 --- a/docs/examples/schema1.py +++ b/docs/examples/schema1.py @@ -36,4 +36,4 @@ class Config: # 'properties': { # 'foo_bar': { # ... -print(MainModel.schema_str(indent=2)) +print(MainModel.schema_json(indent=2)) diff --git a/docs/index.rst b/docs/index.rst index 92d4a1fc51..cecd171156 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -231,7 +231,7 @@ Outputs: The generated schemas are compliant with the specifications: `JSON Schema Core `__, `JSON Schema Validation `__ and `OpenAPI `__. -``BaseModel.schema`` will return a dict of the schema, while ``BaseModel.schema_str`` will return a JSON string representation of that. +``BaseModel.schema`` will return a dict of the schema, while ``BaseModel.schema_json`` will return a JSON string representation of that. "submodels" used are added to the ``definitons`` JSON attribute and referenced, as per the spec. diff --git a/pydantic/main.py b/pydantic/main.py index 2b41e42d75..621bfa5f63 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -327,7 +327,7 @@ def schema(cls, by_alias=True) -> Dict[str, Any]: return s @classmethod - def schema_str(cls, *, by_alias=True, **dumps_kwargs) -> str: + def schema_json(cls, *, by_alias=True, **dumps_kwargs) -> str: from .json import pydantic_encoder return json.dumps(cls.schema(by_alias=by_alias), default=pydantic_encoder, **dumps_kwargs) diff --git a/tests/test_schema.py b/tests/test_schema.py index 85cf95f4c8..acedf9cb8f 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -187,7 +187,7 @@ class Model(BaseModel): a = b'foobar' b = Decimal('12.34') - assert Model.schema_str(indent=2) == ( + assert Model.schema_json(indent=2) == ( '{\n' ' "title": "Model",\n' ' "type": "object",\n' From 8f401167341a47b0d32e19d81e5cd665aef407df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 19:39:36 +0400 Subject: [PATCH 39/65] Remove unnecessary assert in schema module as requested --- pydantic/schema.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index e01c5ddc04..b27017ac39 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -14,7 +14,6 @@ def get_flat_models_from_model(model: Type['main.BaseModel']) -> Set[Type['main.BaseModel']]: flat_models: Set[Type[main.BaseModel]] = set() - assert issubclass(model, main.BaseModel) flat_models.add(model) for field in model.__fields__.values(): flat_models = flat_models | get_flat_models_from_field(field) From 16ca54602754c2f7e92fa126bede2e44d21d446d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 19:42:13 +0400 Subject: [PATCH 40/65] Remove annotations in internal functions, as requested --- pydantic/schema.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index b27017ac39..414f3c9e98 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -13,7 +13,7 @@ def get_flat_models_from_model(model: Type['main.BaseModel']) -> Set[Type['main.BaseModel']]: - flat_models: Set[Type[main.BaseModel]] = set() + flat_models = set() flat_models.add(model) for field in model.__fields__.values(): flat_models = flat_models | get_flat_models_from_field(field) @@ -21,7 +21,7 @@ def get_flat_models_from_model(model: Type['main.BaseModel']) -> Set[Type['main. def get_flat_models_from_field(field: Field) -> Set[Type['main.BaseModel']]: - flat_models: Set[Type[main.BaseModel]] = set() + flat_models = set() if field.sub_fields: flat_models = flat_models | get_flat_models_from_sub_fields(field.sub_fields) elif isinstance(field.type_, type) and issubclass(field.type_, main.BaseModel): @@ -30,14 +30,14 @@ def get_flat_models_from_field(field: Field) -> Set[Type['main.BaseModel']]: def get_flat_models_from_sub_fields(fields) -> Set[Type['main.BaseModel']]: - flat_models: Set[Type[main.BaseModel]] = set() + flat_models = set() for field in fields: flat_models = flat_models | get_flat_models_from_field(field) return flat_models def get_flat_models_from_models(models: Sequence[Type['main.BaseModel']]) -> Set[Type['main.BaseModel']]: - flat_models: Set[Type[main.BaseModel]] = set() + flat_models = set() for model in models: flat_models = flat_models | get_flat_models_from_model(model) return flat_models @@ -53,8 +53,8 @@ def get_long_model_name(model: Type['main.BaseModel']): def get_model_name_maps( unique_models: Set[Type['main.BaseModel']] ) -> Tuple[Dict[str, Type['main.BaseModel']], Dict[Type['main.BaseModel'], str]]: - name_model_map: Dict[str, Type[main.BaseModel]] = {} - conflicting_names: Set[str] = set() + name_model_map = {} + conflicting_names = set() for model in unique_models: model_name = model.__name__ if model_name in conflicting_names: From c9ff829feb80aaf53bf5ff550f19610d56dc172d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 19:52:05 +0400 Subject: [PATCH 41/65] Refactor get_flat_models_from_fields and reuse --- pydantic/schema.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index 414f3c9e98..bfad2a6a70 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -15,21 +15,20 @@ def get_flat_models_from_model(model: Type['main.BaseModel']) -> Set[Type['main.BaseModel']]: flat_models = set() flat_models.add(model) - for field in model.__fields__.values(): - flat_models = flat_models | get_flat_models_from_field(field) + flat_models = flat_models | get_flat_models_from_fields(model.__fields__.values()) return flat_models def get_flat_models_from_field(field: Field) -> Set[Type['main.BaseModel']]: flat_models = set() if field.sub_fields: - flat_models = flat_models | get_flat_models_from_sub_fields(field.sub_fields) + flat_models = flat_models | get_flat_models_from_fields(field.sub_fields) elif isinstance(field.type_, type) and issubclass(field.type_, main.BaseModel): flat_models = flat_models | get_flat_models_from_model(field.type_) return flat_models -def get_flat_models_from_sub_fields(fields) -> Set[Type['main.BaseModel']]: +def get_flat_models_from_fields(fields) -> Set[Type['main.BaseModel']]: flat_models = set() for field in fields: flat_models = flat_models | get_flat_models_from_field(field) From 9d38476787c387165fdbfb7e99b553a7e8eb9a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 19:53:35 +0400 Subject: [PATCH 42/65] Use set short assignment syntax in schema module --- pydantic/schema.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index bfad2a6a70..a35ba182a8 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -15,30 +15,30 @@ def get_flat_models_from_model(model: Type['main.BaseModel']) -> Set[Type['main.BaseModel']]: flat_models = set() flat_models.add(model) - flat_models = flat_models | get_flat_models_from_fields(model.__fields__.values()) + flat_models |= get_flat_models_from_fields(model.__fields__.values()) return flat_models def get_flat_models_from_field(field: Field) -> Set[Type['main.BaseModel']]: flat_models = set() if field.sub_fields: - flat_models = flat_models | get_flat_models_from_fields(field.sub_fields) + flat_models |= get_flat_models_from_fields(field.sub_fields) elif isinstance(field.type_, type) and issubclass(field.type_, main.BaseModel): - flat_models = flat_models | get_flat_models_from_model(field.type_) + flat_models |= get_flat_models_from_model(field.type_) return flat_models def get_flat_models_from_fields(fields) -> Set[Type['main.BaseModel']]: flat_models = set() for field in fields: - flat_models = flat_models | get_flat_models_from_field(field) + flat_models |= get_flat_models_from_field(field) return flat_models def get_flat_models_from_models(models: Sequence[Type['main.BaseModel']]) -> Set[Type['main.BaseModel']]: flat_models = set() for model in models: - flat_models = flat_models | get_flat_models_from_model(model) + flat_models |= get_flat_models_from_model(model) return flat_models From 75963a330b8301e249caec4f4322dd10399bbb23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 19:55:11 +0400 Subject: [PATCH 43/65] Remove unwanted assertion --- pydantic/schema.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index a35ba182a8..7a2c851958 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -43,7 +43,6 @@ def get_flat_models_from_models(models: Sequence[Type['main.BaseModel']]) -> Set def get_long_model_name(model: Type['main.BaseModel']): - assert issubclass(model, main.BaseModel) prefix = model.__module__.replace('.', '__') name = model.__name__ return f'{prefix}__{name}' From b253ac8da5d61f01c938b3f486949ea268bf3d21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 19:56:32 +0400 Subject: [PATCH 44/65] Make get_long_model_name a single line f-string --- pydantic/schema.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index 7a2c851958..a201a26bb7 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -43,9 +43,7 @@ def get_flat_models_from_models(models: Sequence[Type['main.BaseModel']]) -> Set def get_long_model_name(model: Type['main.BaseModel']): - prefix = model.__module__.replace('.', '__') - name = model.__name__ - return f'{prefix}__{name}' + return f'{model.__module__}__{model.__name__}'.replace('.', '__') def get_model_name_maps( From eae52f594987fd9dc20c163cecf032adb7e16f62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 20:08:12 +0400 Subject: [PATCH 45/65] Update model_name_map, add docstring and remove first return value --- pydantic/schema.py | 17 +++++++++++++---- tests/test_schema.py | 12 ++---------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index a201a26bb7..0a7f4c1d8c 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -46,9 +46,18 @@ def get_long_model_name(model: Type['main.BaseModel']): return f'{model.__module__}__{model.__name__}'.replace('.', '__') -def get_model_name_maps( +def get_model_name_map( unique_models: Set[Type['main.BaseModel']] ) -> Tuple[Dict[str, Type['main.BaseModel']], Dict[Type['main.BaseModel'], str]]: + """ + Process a set of models and generate unique names for them to be used as keys in the JSON Schema + definitions. By default the names are the same class name. But if two models in diferent Python + modules have the same name (e.g. "users.Model" and "items.Model"), the generated names will be + based on the Python module path for those conflicting models to prevent name collisions. + + :param unique_models: a Python set of models + :return: dict mapping models to names + """ name_model_map = {} conflicting_names = set() for model in unique_models: @@ -67,7 +76,7 @@ def get_model_name_maps( else: name_model_map[model_name] = model model_name_map = {v: k for k, v in name_model_map.items()} - return name_model_map, model_name_map + return model_name_map def field_schema( @@ -350,7 +359,7 @@ def field_singleton_schema( # noqa: C901 (ignore complexity) def model_schema(class_: 'main.BaseModel', by_alias=True, ref_prefix='#/definitions/') -> Dict[str, Any]: flat_models = get_flat_models_from_model(class_) - _, model_name_map = get_model_name_maps(flat_models) + model_name_map = get_model_name_map(flat_models) m_schema, m_definitions = model_process_schema( class_, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix ) @@ -368,7 +377,7 @@ def schema( ref_prefix='#/definitions/', ) -> Dict: flat_models = get_flat_models_from_models(models) - _, model_name_map = get_model_name_maps(flat_models) + model_name_map = get_model_name_map(flat_models) definitions = {} output_schema = {} if title: diff --git a/tests/test_schema.py b/tests/test_schema.py index acedf9cb8f..c617399655 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -8,7 +8,7 @@ import pytest from pydantic import BaseModel, Schema, ValidationError -from pydantic.schema import get_flat_models_from_model, get_flat_models_from_models, get_model_name_maps, schema +from pydantic.schema import get_flat_models_from_model, get_flat_models_from_models, get_model_name_map, schema from pydantic.types import ( DSN, UUID1, @@ -600,15 +600,7 @@ class Baz(BaseModel): c: Bar flat_models = get_flat_models_from_models([Baz, ModelA, ModelB, ModelC, ModelD]) - name_model_map, model_name_map = get_model_name_maps(flat_models) - assert name_model_map == { - 'Foo': Foo, - 'Bar': Bar, - 'Baz': Baz, - 'tests__schema_test_package__modulea__modela__Model': ModelA, - 'tests__schema_test_package__moduleb__modelb__Model': ModelB, - 'tests__schema_test_package__moduled__modeld__Model': ModelD, - } + model_name_map = get_model_name_map(flat_models) assert model_name_map == { Foo: 'Foo', Bar: 'Bar', From 095aaa0fe05d5f4718255cd2090945ecee8274f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 20:10:33 +0400 Subject: [PATCH 46/65] Simplify dict operation in get_model_name_map as requested --- pydantic/schema.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index 0a7f4c1d8c..e88899c098 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -67,8 +67,7 @@ def get_model_name_map( name_model_map[model_name] = model elif model_name in name_model_map: conflicting_names.add(model_name) - conflicting_model = name_model_map[model_name] - del name_model_map[model_name] + conflicting_model = name_model_map.pop(model_name) conflicting_model_name = get_long_model_name(conflicting_model) name_model_map[conflicting_model_name] = conflicting_model model_name = get_long_model_name(model) From 8bc6fe7acfe0d6cf8a09fcdf986f4464bbbb77f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 20:12:27 +0400 Subject: [PATCH 47/65] Make more concise model_name_map computation --- pydantic/schema.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index e88899c098..0067f1ff4d 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -68,10 +68,8 @@ def get_model_name_map( elif model_name in name_model_map: conflicting_names.add(model_name) conflicting_model = name_model_map.pop(model_name) - conflicting_model_name = get_long_model_name(conflicting_model) - name_model_map[conflicting_model_name] = conflicting_model - model_name = get_long_model_name(model) - name_model_map[model_name] = model + name_model_map[get_long_model_name(conflicting_model)] = conflicting_model + name_model_map[get_long_model_name(model)] = model else: name_model_map[model_name] = model model_name_map = {v: k for k, v in name_model_map.items()} From 6f0815f5cf75bf27c74678c9a122d1d6a7a8feb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 20:13:30 +0400 Subject: [PATCH 48/65] Remove bool from field check in schema as is subclass of int --- pydantic/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index 0067f1ff4d..6d42b99ef3 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -86,7 +86,7 @@ def field_schema( if not field.required and field.default is not None: schema_overrides = True - if isinstance(field.default, (int, float, bool, str)): + if isinstance(field.default, (int, float, str)): s['default'] = field.default else: s['default'] = pydantic_encoder(field.default) From e883bfee76ae8526bf8c66b4b4142637cd2145f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 20:28:35 +0400 Subject: [PATCH 49/65] Make ref_prefix default to None and use global default --- pydantic/schema.py | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index 6d42b99ef3..a564b0a00d 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -12,6 +12,9 @@ from .utils import clean_docstring +default_prefix='#/definitions/' + + def get_flat_models_from_model(model: Type['main.BaseModel']) -> Set[Type['main.BaseModel']]: flat_models = set() flat_models.add(model) @@ -77,8 +80,22 @@ def get_model_name_map( def field_schema( - field: Field, *, by_alias=True, model_name_map: Dict[Type['main.BaseModel'], str], ref_prefix='#/definitions/' + field: Field, *, by_alias=True, model_name_map: Dict[Type['main.BaseModel'], str], ref_prefix=None ) -> Tuple[Dict[str, Any], Dict[str, Any]]: + """ + Process a Pydantic field and return a tuple with a JSON Schema for it as the first item. + Also return a dictionary of definitions with models as keys and their schemas as values. If the passed field + is a model and has submodels, and those submodels don't have overrides (as ``title``, ``default``, etc), they + will be included in the definitions and referenced in the schema instead of included recursively. + + :param field: a Pydantic Field + :param by_alias: use the defined alias (if any) in the returned schema + :param model_name_map: used to generate the JSON Schema references to other models included in the definitions + :param ref_prefix: the JSON Pointer prefix to use for references to other schemas, if None, the default of + #/definitions/ will be used + :return: tuple of the schema for this field and additional definitions + """ + ref_prefix = ref_prefix or default_prefix schema_overrides = False s = dict(title=field._schema.title or field.alias.title()) if field._schema.title: @@ -115,12 +132,13 @@ def field_type_schema( by_alias: bool, model_name_map: Dict[Type['main.BaseModel'], str], schema_overrides=False, - ref_prefix='#/definitions/', + ref_prefix=None, ) -> Tuple[Dict[str, Any], Dict[str, Any]]: definitions = {} + ref_prefix = ref_prefix or default_prefix if field.shape is Shape.LIST: f_schema, f_definitions = field_singleton_schema( - field, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix + field, by_alias=by_alias, model_name_map=model_name_map, ref_prefix= ref_prefix ) definitions.update(f_definitions) return {'type': 'array', 'items': f_schema}, definitions @@ -170,8 +188,9 @@ def model_process_schema( *, by_alias=True, model_name_map: Dict[Type['main.BaseModel'], str], - ref_prefix='#/definitions/', + ref_prefix=None, ) -> Tuple[Dict[str, Any], Dict[str, Any]]: + ref_prefix = ref_prefix or default_prefix s = {'title': class_.__config__.title or class_.__name__} if class_.__doc__: s['description'] = clean_docstring(class_.__doc__) @@ -187,8 +206,9 @@ def model_type_schema( *, by_alias: bool, model_name_map: Dict[Type['main.BaseModel'], str], - ref_prefix='#/definitions/', + ref_prefix=None, ): + ref_prefix = ref_prefix or default_prefix properties = {} required = [] definitions = {} @@ -217,8 +237,9 @@ def field_singleton_sub_fields_schema( by_alias: bool, model_name_map: Dict[Type['main.BaseModel'], str], schema_overrides=False, - ref_prefix='#/definitions/', + ref_prefix=None, ) -> Tuple[Dict[str, Any], Dict[str, Any]]: + ref_prefix = ref_prefix or default_prefix definitions = {} if len(sub_fields) == 1: return field_type_schema( @@ -249,8 +270,9 @@ def field_singleton_schema( # noqa: C901 (ignore complexity) by_alias: bool, model_name_map: Dict[Type['main.BaseModel'], str], schema_overrides=False, - ref_prefix='#/definitions/', + ref_prefix=None, ) -> Tuple[Dict[str, Any], Dict[str, Any]]: + ref_prefix = ref_prefix or default_prefix definitions = {} if field.sub_fields: return field_singleton_sub_fields_schema( @@ -354,7 +376,8 @@ def field_singleton_schema( # noqa: C901 (ignore complexity) raise ValueError(f'Value not declarable with JSON Schema, field: {field}') -def model_schema(class_: 'main.BaseModel', by_alias=True, ref_prefix='#/definitions/') -> Dict[str, Any]: +def model_schema(class_: 'main.BaseModel', by_alias=True, ref_prefix=None) -> Dict[str, Any]: + ref_prefix = ref_prefix or default_prefix flat_models = get_flat_models_from_model(class_) model_name_map = get_model_name_map(flat_models) m_schema, m_definitions = model_process_schema( @@ -371,8 +394,9 @@ def schema( by_alias=True, title=None, description=None, - ref_prefix='#/definitions/', + ref_prefix=None, ) -> Dict: + ref_prefix = ref_prefix or default_prefix flat_models = get_flat_models_from_models(models) model_name_map = get_model_name_map(flat_models) definitions = {} From b3c4262999fdd36d322618fece16f7e298ab94c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 20:30:27 +0400 Subject: [PATCH 50/65] Fix formatting for schema.py --- pydantic/schema.py | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index a564b0a00d..d67b887f03 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -11,8 +11,7 @@ from .types import DSN, UUID1, UUID3, UUID4, UUID5, DirectoryPath, EmailStr, FilePath, Json, NameEmail, UrlStr from .utils import clean_docstring - -default_prefix='#/definitions/' +default_prefix = '#/definitions/' def get_flat_models_from_model(model: Type['main.BaseModel']) -> Set[Type['main.BaseModel']]: @@ -138,7 +137,7 @@ def field_type_schema( ref_prefix = ref_prefix or default_prefix if field.shape is Shape.LIST: f_schema, f_definitions = field_singleton_schema( - field, by_alias=by_alias, model_name_map=model_name_map, ref_prefix= ref_prefix + field, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix ) definitions.update(f_definitions) return {'type': 'array', 'items': f_schema}, definitions @@ -184,11 +183,7 @@ def field_type_schema( def model_process_schema( - class_: Type['main.BaseModel'], - *, - by_alias=True, - model_name_map: Dict[Type['main.BaseModel'], str], - ref_prefix=None, + class_: Type['main.BaseModel'], *, by_alias=True, model_name_map: Dict[Type['main.BaseModel'], str], ref_prefix=None ) -> Tuple[Dict[str, Any], Dict[str, Any]]: ref_prefix = ref_prefix or default_prefix s = {'title': class_.__config__.title or class_.__name__} @@ -202,11 +197,7 @@ def model_process_schema( def model_type_schema( - class_: 'main.BaseModel', - *, - by_alias: bool, - model_name_map: Dict[Type['main.BaseModel'], str], - ref_prefix=None, + class_: 'main.BaseModel', *, by_alias: bool, model_name_map: Dict[Type['main.BaseModel'], str], ref_prefix=None ): ref_prefix = ref_prefix or default_prefix properties = {} @@ -389,12 +380,7 @@ def model_schema(class_: 'main.BaseModel', by_alias=True, ref_prefix=None) -> Di def schema( - models: Sequence[Type['main.BaseModel']], - *, - by_alias=True, - title=None, - description=None, - ref_prefix=None, + models: Sequence[Type['main.BaseModel']], *, by_alias=True, title=None, description=None, ref_prefix=None ) -> Dict: ref_prefix = ref_prefix or default_prefix flat_models = get_flat_models_from_models(models) From edf2cc7bd8d2431692476adca3715fbd5cf7376c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 21:05:04 +0400 Subject: [PATCH 51/65] Refactor field_singleton_schema to use data structures --- pydantic/schema.py | 130 ++++++++++++++++++++------------------------- 1 file changed, 57 insertions(+), 73 deletions(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index d67b887f03..0bb4a6acdd 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -13,6 +13,48 @@ default_prefix = '#/definitions/' +validation_attribute_to_schema_keyword = { + 'min_length': 'minLength', + 'max_length': 'maxLength', + 'regex': 'pattern', + 'gt': 'exclusiveMinimum', + 'lt': 'exclusiveMaximum', + 'ge': 'minimum', + 'le': 'maximum', +} + +# Order is important, subclasses of str must go before str, etc +field_class_to_schema_enum_enabled = ( + (EmailStr, {'type': 'string', 'format': 'email'}), + (UrlStr, {'type': 'string', 'format': 'uri'}), + (DSN, {'type': 'string', 'format': 'dsn'}), + (str, {'type': 'string'}), + (bytes, {'type': 'string', 'format': 'binary'}), + (bool, {'type': 'boolean'}), + (int, {'type': 'integer'}), + (float, {'type': 'number'}), + (Decimal, {'type': 'number'}), + (UUID1, {'type': 'string', 'format': 'uuid1'}), + (UUID3, {'type': 'string', 'format': 'uuid3'}), + (UUID4, {'type': 'string', 'format': 'uuid4'}), + (UUID5, {'type': 'string', 'format': 'uuid5'}), + (UUID, {'type': 'string', 'format': 'uuid'}), + (NameEmail, {'type': 'string', 'format': 'name-email'}), +) + + +# Order is important, subclasses of Path must go before Path, etc +field_class_to_schema_enum_disabled = ( + (FilePath, {'type': 'string', 'format': 'file-path'}), + (DirectoryPath, {'type': 'string', 'format': 'directory-path'}), + (Path, {'type': 'string', 'format': 'path'}), + (datetime, {'type': 'string', 'format': 'date-time'}), + (date, {'type': 'string', 'format': 'date'}), + (time, {'type': 'string', 'format': 'time'}), + (timedelta, {'type': 'string', 'format': 'time-delta'}), + (Json, {'type': 'string', 'format': 'json-string'}), +) + def get_flat_models_from_model(model: Type['main.BaseModel']) -> Set[Type['main.BaseModel']]: flat_models = set() @@ -279,81 +321,23 @@ def field_singleton_schema( # noqa: C901 (ignore complexity) if issubclass(field.type_, Enum): f_schema.update({'enum': [item.value for item in field.type_]}) # Don't return immediately, to allow adding specific types - # For constrained strings - if hasattr(field.type_, 'min_length'): - if field.type_.min_length is not None: - f_schema.update({'minLength': field.type_.min_length}) - if hasattr(field.type_, 'max_length'): - if field.type_.max_length is not None: - f_schema.update({'maxLength': field.type_.max_length}) - if hasattr(field.type_, 'regex'): - if field.type_.regex is not None: - f_schema.update({'pattern': field.type_.regex.pattern}) - # For constrained numbers - if hasattr(field.type_, 'gt'): - if field.type_.gt is not None: - f_schema.update({'exclusiveMinimum': field.type_.gt}) - if hasattr(field.type_, 'lt'): - if field.type_.lt is not None: - f_schema.update({'exclusiveMaximum': field.type_.lt}) - if hasattr(field.type_, 'ge'): - if field.type_.ge is not None: - f_schema.update({'minimum': field.type_.ge}) - if hasattr(field.type_, 'le'): - if field.type_.le is not None: - f_schema.update({'maximum': field.type_.le}) - # Sub-classes of str must go before str - if issubclass(field.type_, EmailStr): - f_schema.update({'type': 'string', 'format': 'email'}) - elif issubclass(field.type_, UrlStr): - f_schema.update({'type': 'string', 'format': 'uri'}) - elif issubclass(field.type_, DSN): - f_schema.update({'type': 'string', 'format': 'dsn'}) - elif issubclass(field.type_, str): - f_schema.update({'type': 'string'}) - elif issubclass(field.type_, bytes): - f_schema.update({'type': 'string', 'format': 'binary'}) - elif issubclass(field.type_, bool): - f_schema.update({'type': 'boolean'}) - elif issubclass(field.type_, int): - f_schema.update({'type': 'integer'}) - elif issubclass(field.type_, float): - f_schema.update({'type': 'number'}) - elif issubclass(field.type_, Decimal): - f_schema.update({'type': 'number'}) - elif issubclass(field.type_, UUID1): - f_schema.update({'type': 'string', 'format': 'uuid1'}) - elif issubclass(field.type_, UUID3): - f_schema.update({'type': 'string', 'format': 'uuid3'}) - elif issubclass(field.type_, UUID4): - f_schema.update({'type': 'string', 'format': 'uuid4'}) - elif issubclass(field.type_, UUID5): - f_schema.update({'type': 'string', 'format': 'uuid5'}) - elif issubclass(field.type_, UUID): - f_schema.update({'type': 'string', 'format': 'uuid'}) - elif issubclass(field.type_, NameEmail): - f_schema.update({'type': 'string', 'format': 'name-email'}) - # This is the last value that can also be an Enum + for field_name, schema_name in validation_attribute_to_schema_keyword.items(): + field_value = getattr(field.type_, field_name, None) + if field_value is not None: + if field_name == 'regex': + field_value = field_value.pattern + f_schema[schema_name] = field_value + for type_, t_schema in field_class_to_schema_enum_enabled: + if issubclass(field.type_, type_): + f_schema.update(t_schema) + break + # Return schema, with or without enum definitions if f_schema: return f_schema, definitions - # Path subclasses must go before Path - elif issubclass(field.type_, FilePath): - return {'type': 'string', 'format': 'file-path'}, definitions - elif issubclass(field.type_, DirectoryPath): - return {'type': 'string', 'format': 'directory-path'}, definitions - elif issubclass(field.type_, Path): - return {'type': 'string', 'format': 'path'}, definitions - elif issubclass(field.type_, datetime): - return {'type': 'string', 'format': 'date-time'}, definitions - elif issubclass(field.type_, date): - return {'type': 'string', 'format': 'date'}, definitions - elif issubclass(field.type_, time): - return {'type': 'string', 'format': 'time'}, definitions - elif issubclass(field.type_, timedelta): - return {'type': 'string', 'format': 'time-delta'}, definitions - elif issubclass(field.type_, Json): - return {'type': 'string', 'format': 'json-string'}, definitions - elif issubclass(field.type_, main.BaseModel): + for type_, t_schema in field_class_to_schema_enum_disabled: + if issubclass(field.type_, type_): + return t_schema, definitions + if issubclass(field.type_, main.BaseModel): sub_schema, sub_definitions = model_process_schema( field.type_, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix ) From ec03e2248065fb74a4ca6cc1e9e94d1a73076337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 21:25:53 +0400 Subject: [PATCH 52/65] Move main functions to top of schema, and add docstrings for them --- pydantic/schema.py | 215 +++++++++++++++++++++++++-------------------- 1 file changed, 122 insertions(+), 93 deletions(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index 0bb4a6acdd..8f88d3b43e 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -56,68 +56,69 @@ ) -def get_flat_models_from_model(model: Type['main.BaseModel']) -> Set[Type['main.BaseModel']]: - flat_models = set() - flat_models.add(model) - flat_models |= get_flat_models_from_fields(model.__fields__.values()) - return flat_models - - -def get_flat_models_from_field(field: Field) -> Set[Type['main.BaseModel']]: - flat_models = set() - if field.sub_fields: - flat_models |= get_flat_models_from_fields(field.sub_fields) - elif isinstance(field.type_, type) and issubclass(field.type_, main.BaseModel): - flat_models |= get_flat_models_from_model(field.type_) - return flat_models - - -def get_flat_models_from_fields(fields) -> Set[Type['main.BaseModel']]: - flat_models = set() - for field in fields: - flat_models |= get_flat_models_from_field(field) - return flat_models - - -def get_flat_models_from_models(models: Sequence[Type['main.BaseModel']]) -> Set[Type['main.BaseModel']]: - flat_models = set() +def schema( + models: Sequence[Type['main.BaseModel']], *, by_alias=True, title=None, description=None, ref_prefix=None +) -> Dict: + """ + Process a list of models and generate a single JSON Schema with all of them defined in the ``definitions`` + top-level JSON key, including their submodels. + + :param models: a list of models to include in the generated JSON Schema + :param by_alias: generate the schemas using the aliases defined, if any + :param title: title for the generated schema that includes the definitions + :param description: description for the generated schema + :param ref_prefix: the JSON Pointer prefix for schema references with ``$ref``, if None, will be set to the + default of ``#/definitions/``. Update it if you want the schemas to reference the definitions somewhere + else, e.g. for OpenAPI use ``#/components/schemas/``. The resulting generated schemas will still be at the + top-level key ``definitions``, so you can extract them from there. But all the references will have the set + prefix. + :return: dict with the JSON Schema with a ``definitions`` top-level key including the schema definitions for + the models and submodels passed in ``models``. + """ + ref_prefix = ref_prefix or default_prefix + flat_models = get_flat_models_from_models(models) + model_name_map = get_model_name_map(flat_models) + definitions = {} + output_schema = {} + if title: + output_schema['title'] = title + if description: + output_schema['description'] = description for model in models: - flat_models |= get_flat_models_from_model(model) - return flat_models - - -def get_long_model_name(model: Type['main.BaseModel']): - return f'{model.__module__}__{model.__name__}'.replace('.', '__') + m_schema, m_definitions = model_process_schema( + model, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix + ) + definitions.update(m_definitions) + model_name = model_name_map[model] + definitions[model_name] = m_schema + if definitions: + output_schema['definitions'] = definitions + return output_schema -def get_model_name_map( - unique_models: Set[Type['main.BaseModel']] -) -> Tuple[Dict[str, Type['main.BaseModel']], Dict[Type['main.BaseModel'], str]]: +def model_schema(model: 'main.BaseModel', by_alias=True, ref_prefix=None) -> Dict[str, Any]: """ - Process a set of models and generate unique names for them to be used as keys in the JSON Schema - definitions. By default the names are the same class name. But if two models in diferent Python - modules have the same name (e.g. "users.Model" and "items.Model"), the generated names will be - based on the Python module path for those conflicting models to prevent name collisions. - - :param unique_models: a Python set of models - :return: dict mapping models to names + Generate a JSON Schema for one model. With all the submodels defined in the ``definitions`` top-level + JSON key. + + :param model: a Pydantic model (a class that inherits from BaseModel) + :param by_alias: generate the schemas using the aliases defined, if any + :param ref_prefix: the JSON Pointer prefix for schema references with ``$ref``, if None, will be set to the + default of ``#/definitions/``. Update it if you want the schemas to reference the definitions somewhere + else, e.g. for OpenAPI use ``#/components/schemas/``. The resulting generated schemas will still be at the + top-level key ``definitions``, so you can extract them from there. But all the references will have the set + prefix. + :return: dict with the JSON Schema for the passed ``model`` """ - name_model_map = {} - conflicting_names = set() - for model in unique_models: - model_name = model.__name__ - if model_name in conflicting_names: - model_name = get_long_model_name(model) - name_model_map[model_name] = model - elif model_name in name_model_map: - conflicting_names.add(model_name) - conflicting_model = name_model_map.pop(model_name) - name_model_map[get_long_model_name(conflicting_model)] = conflicting_model - name_model_map[get_long_model_name(model)] = model - else: - name_model_map[model_name] = model - model_name_map = {v: k for k, v in name_model_map.items()} - return model_name_map + ref_prefix = ref_prefix or default_prefix + flat_models = get_flat_models_from_model(model) + model_name_map = get_model_name_map(flat_models) + m_schema, m_definitions = model_process_schema( + model, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix + ) + if m_definitions: + m_schema.update({'definitions': m_definitions}) + return m_schema def field_schema( @@ -167,6 +168,70 @@ def field_schema( return s, f_definitions +def get_model_name_map( + unique_models: Set[Type['main.BaseModel']] +) -> Tuple[Dict[str, Type['main.BaseModel']], Dict[Type['main.BaseModel'], str]]: + """ + Process a set of models and generate unique names for them to be used as keys in the JSON Schema + definitions. By default the names are the same class name. But if two models in diferent Python + modules have the same name (e.g. "users.Model" and "items.Model"), the generated names will be + based on the Python module path for those conflicting models to prevent name collisions. + + :param unique_models: a Python set of models + :return: dict mapping models to names + """ + name_model_map = {} + conflicting_names = set() + for model in unique_models: + model_name = model.__name__ + if model_name in conflicting_names: + model_name = get_long_model_name(model) + name_model_map[model_name] = model + elif model_name in name_model_map: + conflicting_names.add(model_name) + conflicting_model = name_model_map.pop(model_name) + name_model_map[get_long_model_name(conflicting_model)] = conflicting_model + name_model_map[get_long_model_name(model)] = model + else: + name_model_map[model_name] = model + model_name_map = {v: k for k, v in name_model_map.items()} + return model_name_map + + +def get_flat_models_from_model(model: Type['main.BaseModel']) -> Set[Type['main.BaseModel']]: + flat_models = set() + flat_models.add(model) + flat_models |= get_flat_models_from_fields(model.__fields__.values()) + return flat_models + + +def get_flat_models_from_field(field: Field) -> Set[Type['main.BaseModel']]: + flat_models = set() + if field.sub_fields: + flat_models |= get_flat_models_from_fields(field.sub_fields) + elif isinstance(field.type_, type) and issubclass(field.type_, main.BaseModel): + flat_models |= get_flat_models_from_model(field.type_) + return flat_models + + +def get_flat_models_from_fields(fields) -> Set[Type['main.BaseModel']]: + flat_models = set() + for field in fields: + flat_models |= get_flat_models_from_field(field) + return flat_models + + +def get_flat_models_from_models(models: Sequence[Type['main.BaseModel']]) -> Set[Type['main.BaseModel']]: + flat_models = set() + for model in models: + flat_models |= get_flat_models_from_model(model) + return flat_models + + +def get_long_model_name(model: Type['main.BaseModel']): + return f'{model.__module__}__{model.__name__}'.replace('.', '__') + + def field_type_schema( field: Field, *, @@ -349,39 +414,3 @@ def field_singleton_schema( # noqa: C901 (ignore complexity) else: return sub_schema, definitions raise ValueError(f'Value not declarable with JSON Schema, field: {field}') - - -def model_schema(class_: 'main.BaseModel', by_alias=True, ref_prefix=None) -> Dict[str, Any]: - ref_prefix = ref_prefix or default_prefix - flat_models = get_flat_models_from_model(class_) - model_name_map = get_model_name_map(flat_models) - m_schema, m_definitions = model_process_schema( - class_, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix - ) - if m_definitions: - m_schema.update({'definitions': m_definitions}) - return m_schema - - -def schema( - models: Sequence[Type['main.BaseModel']], *, by_alias=True, title=None, description=None, ref_prefix=None -) -> Dict: - ref_prefix = ref_prefix or default_prefix - flat_models = get_flat_models_from_models(models) - model_name_map = get_model_name_map(flat_models) - definitions = {} - output_schema = {} - if title: - output_schema['title'] = title - if description: - output_schema['description'] = description - for model in models: - m_schema, m_definitions = model_process_schema( - model, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix - ) - definitions.update(m_definitions) - model_name = model_name_map[model] - definitions[model_name] = m_schema - if definitions: - output_schema['definitions'] = definitions - return output_schema From 0a24236c91bb18940affd0474d1ee32e58d1c2a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 21:33:55 +0400 Subject: [PATCH 53/65] Implement __all__, move and order parts of schema --- pydantic/schema.py | 102 ++++++++++++++++++++++++++------------------- 1 file changed, 60 insertions(+), 42 deletions(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index 8f88d3b43e..183476e8d3 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -11,49 +11,24 @@ from .types import DSN, UUID1, UUID3, UUID4, UUID5, DirectoryPath, EmailStr, FilePath, Json, NameEmail, UrlStr from .utils import clean_docstring -default_prefix = '#/definitions/' - -validation_attribute_to_schema_keyword = { - 'min_length': 'minLength', - 'max_length': 'maxLength', - 'regex': 'pattern', - 'gt': 'exclusiveMinimum', - 'lt': 'exclusiveMaximum', - 'ge': 'minimum', - 'le': 'maximum', -} +__all__ = [ + 'schema', + 'model_schema', + 'field_schema', + 'get_model_name_map', + 'get_flat_models_from_model', + 'get_flat_models_from_field', + 'get_flat_models_from_fields', + 'get_flat_models_from_models', + 'get_long_model_name', + 'field_type_schema', + 'model_process_schema', + 'model_type_schema', + 'field_singleton_sub_fields_schema', + 'field_singleton_schema', +] -# Order is important, subclasses of str must go before str, etc -field_class_to_schema_enum_enabled = ( - (EmailStr, {'type': 'string', 'format': 'email'}), - (UrlStr, {'type': 'string', 'format': 'uri'}), - (DSN, {'type': 'string', 'format': 'dsn'}), - (str, {'type': 'string'}), - (bytes, {'type': 'string', 'format': 'binary'}), - (bool, {'type': 'boolean'}), - (int, {'type': 'integer'}), - (float, {'type': 'number'}), - (Decimal, {'type': 'number'}), - (UUID1, {'type': 'string', 'format': 'uuid1'}), - (UUID3, {'type': 'string', 'format': 'uuid3'}), - (UUID4, {'type': 'string', 'format': 'uuid4'}), - (UUID5, {'type': 'string', 'format': 'uuid5'}), - (UUID, {'type': 'string', 'format': 'uuid'}), - (NameEmail, {'type': 'string', 'format': 'name-email'}), -) - - -# Order is important, subclasses of Path must go before Path, etc -field_class_to_schema_enum_disabled = ( - (FilePath, {'type': 'string', 'format': 'file-path'}), - (DirectoryPath, {'type': 'string', 'format': 'directory-path'}), - (Path, {'type': 'string', 'format': 'path'}), - (datetime, {'type': 'string', 'format': 'date-time'}), - (date, {'type': 'string', 'format': 'date'}), - (time, {'type': 'string', 'format': 'time'}), - (timedelta, {'type': 'string', 'format': 'time-delta'}), - (Json, {'type': 'string', 'format': 'json-string'}), -) +default_prefix = '#/definitions/' def schema( @@ -362,6 +337,49 @@ def field_singleton_sub_fields_schema( return {'anyOf': sub_field_schemas}, definitions +validation_attribute_to_schema_keyword = { + 'min_length': 'minLength', + 'max_length': 'maxLength', + 'regex': 'pattern', + 'gt': 'exclusiveMinimum', + 'lt': 'exclusiveMaximum', + 'ge': 'minimum', + 'le': 'maximum', +} + +# Order is important, subclasses of str must go before str, etc +field_class_to_schema_enum_enabled = ( + (EmailStr, {'type': 'string', 'format': 'email'}), + (UrlStr, {'type': 'string', 'format': 'uri'}), + (DSN, {'type': 'string', 'format': 'dsn'}), + (str, {'type': 'string'}), + (bytes, {'type': 'string', 'format': 'binary'}), + (bool, {'type': 'boolean'}), + (int, {'type': 'integer'}), + (float, {'type': 'number'}), + (Decimal, {'type': 'number'}), + (UUID1, {'type': 'string', 'format': 'uuid1'}), + (UUID3, {'type': 'string', 'format': 'uuid3'}), + (UUID4, {'type': 'string', 'format': 'uuid4'}), + (UUID5, {'type': 'string', 'format': 'uuid5'}), + (UUID, {'type': 'string', 'format': 'uuid'}), + (NameEmail, {'type': 'string', 'format': 'name-email'}), +) + + +# Order is important, subclasses of Path must go before Path, etc +field_class_to_schema_enum_disabled = ( + (FilePath, {'type': 'string', 'format': 'file-path'}), + (DirectoryPath, {'type': 'string', 'format': 'directory-path'}), + (Path, {'type': 'string', 'format': 'path'}), + (datetime, {'type': 'string', 'format': 'date-time'}), + (date, {'type': 'string', 'format': 'date'}), + (time, {'type': 'string', 'format': 'time'}), + (timedelta, {'type': 'string', 'format': 'time-delta'}), + (Json, {'type': 'string', 'format': 'json-string'}), +) + + def field_singleton_schema( # noqa: C901 (ignore complexity) field: Field, *, From 033670ba1a15420ee2de34b4c7f7b4d6af6413bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 22:08:58 +0400 Subject: [PATCH 54/65] Remove schema testing sub-package code as requested --- tests/schema_test_package/__init__.py | 0 tests/schema_test_package/modulea/__init__.py | 0 tests/schema_test_package/modulea/modela.py | 5 ----- tests/schema_test_package/moduleb/__init__.py | 0 tests/schema_test_package/moduleb/modelb.py | 5 ----- tests/schema_test_package/modulec/__init__.py | 0 tests/schema_test_package/modulec/modelc.py | 1 - tests/schema_test_package/moduled/__init__.py | 0 tests/schema_test_package/moduled/modeld.py | 5 ----- 9 files changed, 16 deletions(-) delete mode 100644 tests/schema_test_package/__init__.py delete mode 100644 tests/schema_test_package/modulea/__init__.py delete mode 100644 tests/schema_test_package/modulea/modela.py delete mode 100644 tests/schema_test_package/moduleb/__init__.py delete mode 100644 tests/schema_test_package/moduleb/modelb.py delete mode 100644 tests/schema_test_package/modulec/__init__.py delete mode 100644 tests/schema_test_package/modulec/modelc.py delete mode 100644 tests/schema_test_package/moduled/__init__.py delete mode 100644 tests/schema_test_package/moduled/modeld.py diff --git a/tests/schema_test_package/__init__.py b/tests/schema_test_package/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/schema_test_package/modulea/__init__.py b/tests/schema_test_package/modulea/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/schema_test_package/modulea/modela.py b/tests/schema_test_package/modulea/modela.py deleted file mode 100644 index 75ac72d84e..0000000000 --- a/tests/schema_test_package/modulea/modela.py +++ /dev/null @@ -1,5 +0,0 @@ -from pydantic import BaseModel - - -class Model(BaseModel): - a: str diff --git a/tests/schema_test_package/moduleb/__init__.py b/tests/schema_test_package/moduleb/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/schema_test_package/moduleb/modelb.py b/tests/schema_test_package/moduleb/modelb.py deleted file mode 100644 index 81bcbfbde8..0000000000 --- a/tests/schema_test_package/moduleb/modelb.py +++ /dev/null @@ -1,5 +0,0 @@ -from pydantic import BaseModel - - -class Model(BaseModel): - a: int diff --git a/tests/schema_test_package/modulec/__init__.py b/tests/schema_test_package/modulec/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/schema_test_package/modulec/modelc.py b/tests/schema_test_package/modulec/modelc.py deleted file mode 100644 index aed2198f4c..0000000000 --- a/tests/schema_test_package/modulec/modelc.py +++ /dev/null @@ -1 +0,0 @@ -from ..moduleb.modelb import Model # noqa: F401 (ignore imported but unused) diff --git a/tests/schema_test_package/moduled/__init__.py b/tests/schema_test_package/moduled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/schema_test_package/moduled/modeld.py b/tests/schema_test_package/moduled/modeld.py deleted file mode 100644 index 81bcbfbde8..0000000000 --- a/tests/schema_test_package/moduled/modeld.py +++ /dev/null @@ -1,5 +0,0 @@ -from pydantic import BaseModel - - -class Model(BaseModel): - a: int From e22a899e7f3b3aab7ef4f15ab09cec876fddb2d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 22:09:45 +0400 Subject: [PATCH 55/65] Generate schema testing subpackage in code --- tests/test_schema.py | 48 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/tests/test_schema.py b/tests/test_schema.py index c617399655..43c24846fd 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -1,3 +1,6 @@ +import os +import sys +import tempfile from datetime import date, datetime, time, timedelta from decimal import Decimal from enum import Enum, IntEnum @@ -42,11 +45,6 @@ urlstr, ) -from .schema_test_package.modulea.modela import Model as ModelA -from .schema_test_package.moduleb.modelb import Model as ModelB -from .schema_test_package.modulec.modelc import Model as ModelC -from .schema_test_package.moduled.modeld import Model as ModelD - try: import email_validator except ImportError: @@ -549,8 +547,34 @@ class Model(BaseModel): Model.schema() +def create_testing_submodules(): + base_path = Path(tempfile.mkdtemp()) + mod_root_path = base_path / 'pydantic_schema_test' + os.makedirs(mod_root_path, exist_ok=True) + open(mod_root_path / '__init__.py', 'w').close() + for mod in ['a', 'b', 'c']: + module_name = 'module' + mod + model_name = 'model' + mod + '.py' + os.makedirs(mod_root_path / module_name, exist_ok=True) + open(mod_root_path / module_name / '__init__.py', 'w').close() + with open(mod_root_path / module_name / model_name, 'w') as f: + f.write('from pydantic import BaseModel\n' 'class Model(BaseModel):\n' ' a: str\n') + module_name = 'moduled' + model_name = 'modeld.py' + os.makedirs(mod_root_path / module_name, exist_ok=True) + open(mod_root_path / module_name / '__init__.py', 'w').close() + with open(mod_root_path / module_name / model_name, 'w') as f: + f.write('from ..moduleb.modelb import Model') + sys.path.insert(0, str(base_path)) + + def test_flat_models_unique_models(): - flat_models = get_flat_models_from_models([ModelA, ModelB, ModelC]) + create_testing_submodules() + from pydantic_schema_test.modulea.modela import Model as ModelA + from pydantic_schema_test.moduleb.modelb import Model as ModelB + from pydantic_schema_test.moduled.modeld import Model as ModelD + + flat_models = get_flat_models_from_models([ModelA, ModelB, ModelD]) assert flat_models == set([ModelA, ModelB]) @@ -590,6 +614,12 @@ class Pizza(BaseModel): def test_model_name_maps(): + create_testing_submodules() + from pydantic_schema_test.modulea.modela import Model as ModelA + from pydantic_schema_test.moduleb.modelb import Model as ModelB + from pydantic_schema_test.modulec.modelc import Model as ModelC + from pydantic_schema_test.moduled.modeld import Model as ModelD + class Foo(BaseModel): a: str @@ -605,9 +635,9 @@ class Baz(BaseModel): Foo: 'Foo', Bar: 'Bar', Baz: 'Baz', - ModelA: 'tests__schema_test_package__modulea__modela__Model', - ModelB: 'tests__schema_test_package__moduleb__modelb__Model', - ModelD: 'tests__schema_test_package__moduled__modeld__Model', + ModelA: 'pydantic_schema_test__modulea__modela__Model', + ModelB: 'pydantic_schema_test__moduleb__modelb__Model', + ModelC: 'pydantic_schema_test__modulec__modelc__Model', } From a73818e1049850bf963262088bb741c9d89ddbdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 23:09:22 +0400 Subject: [PATCH 56/65] Update schema tests with several related fields to use parametrized pytest --- tests/test_schema.py | 427 +++++++++++++++++++++++-------------------- 1 file changed, 231 insertions(+), 196 deletions(-) diff --git a/tests/test_schema.py b/tests/test_schema.py index 43c24846fd..9308e958ac 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -260,29 +260,34 @@ class Model(BaseModel): } -def test_tuple(): +@pytest.mark.parametrize( + 'field_type,expected_schema', + [ + ( + Tuple[str, int, Union[str, int, float], float], + [ + {'type': 'string'}, + {'type': 'integer'}, + {'anyOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'number'}]}, + {'type': 'number'}, + ], + ), + (Tuple[str], {'type': 'string'}), + ], +) +def test_tuple(field_type, expected_schema): class Model(BaseModel): - a: Tuple[str, int, Union[str, int, float], float] - b: Tuple[str] + a: field_type - assert Model.schema() == { + base_schema = { 'title': 'Model', 'type': 'object', - 'properties': { - 'a': { - 'title': 'A', - 'type': 'array', - 'items': [ - {'type': 'string'}, - {'type': 'integer'}, - {'anyOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'number'}]}, - {'type': 'number'}, - ], - }, - 'b': {'title': 'B', 'type': 'array', 'items': {'type': 'string'}}, - }, - 'required': ['a', 'b'], + 'properties': {'a': {'title': 'A', 'type': 'array', 'items': None}}, + 'required': ['a'], } + base_schema['properties']['a']['items'] = expected_schema + + assert Model.schema() == base_schema def test_bool(): @@ -297,233 +302,266 @@ class Model(BaseModel): } -def test_list_union_dict(): - class Foo(BaseModel): - a: float - - class Model(BaseModel): - """party time""" +class Foo(BaseModel): + a: float - a: Union[int, str] - b: List[int] - c: Dict[str, Foo] - d: Union[None, Foo] - e: Dict[str, Any] - model_schema = Model.schema() - assert model_schema == { - 'title': 'Model', - 'description': 'party time', - 'type': 'object', - 'definitions': { - 'Foo': { - 'title': 'Foo', - 'type': 'object', - 'properties': {'a': {'title': 'A', 'type': 'number'}}, +@pytest.mark.parametrize( + 'field_type,expected_schema', + [ + ( + Union[int, str], + { + 'properties': {'a': {'title': 'A', 'anyOf': [{'type': 'integer'}, {'type': 'string'}]}}, 'required': ['a'], - } - }, - 'properties': { - 'a': {'title': 'A', 'anyOf': [{'type': 'integer'}, {'type': 'string'}]}, - 'b': {'title': 'B', 'type': 'array', 'items': {'type': 'integer'}}, - 'c': {'title': 'C', 'type': 'object', 'additionalProperties': {'$ref': '#/definitions/Foo'}}, - 'd': {'$ref': '#/definitions/Foo'}, - 'e': {'title': 'E', 'type': 'object'}, - }, - 'required': ['a', 'b', 'c', 'e'], - } - - -def test_date_types(): + }, + ), + ( + List[int], + {'properties': {'a': {'title': 'A', 'type': 'array', 'items': {'type': 'integer'}}}, 'required': ['a']}, + ), + ( + Dict[str, Foo], + { + 'definitions': { + 'Foo': { + 'title': 'Foo', + 'type': 'object', + 'properties': {'a': {'title': 'A', 'type': 'number'}}, + 'required': ['a'], + } + }, + 'properties': { + 'a': {'title': 'A', 'type': 'object', 'additionalProperties': {'$ref': '#/definitions/Foo'}} + }, + 'required': ['a'], + }, + ), + ( + Union[None, Foo], + { + 'definitions': { + 'Foo': { + 'title': 'Foo', + 'type': 'object', + 'properties': {'a': {'title': 'A', 'type': 'number'}}, + 'required': ['a'], + } + }, + 'properties': {'a': {'$ref': '#/definitions/Foo'}}, + }, + ), + (Dict[str, Any], {'properties': {'a': {'title': 'A', 'type': 'object'}}, 'required': ['a']}), + ], +) +def test_list_union_dict(field_type, expected_schema): class Model(BaseModel): - a: datetime - b: date - c: time - d: timedelta + a: field_type - model_schema = Model.schema() - assert model_schema == { - 'title': 'Model', - 'type': 'object', - 'properties': { - 'a': {'title': 'A', 'type': 'string', 'format': 'date-time'}, - 'b': {'title': 'B', 'type': 'string', 'format': 'date'}, - 'c': {'title': 'C', 'type': 'string', 'format': 'time'}, - 'd': {'title': 'D', 'type': 'string', 'format': 'time-delta'}, - }, - 'required': ['a', 'b', 'c', 'd'], - } + base_schema = {'title': 'Model', 'type': 'object'} + base_schema.update(expected_schema) + assert Model.schema() == base_schema -def test_str_basic_types(): + +@pytest.mark.parametrize( + 'field_type,expected_schema', [(datetime, 'date-time'), (date, 'date'), (time, 'time'), (timedelta, 'time-delta')] +) +def test_date_types(field_type, expected_schema): class Model(BaseModel): - a: NoneStr - b: NoneBytes - c: StrBytes - d: NoneStrBytes + a: field_type - model_schema = Model.schema() - assert model_schema == { + base_schema = { 'title': 'Model', 'type': 'object', - 'properties': { - 'a': {'title': 'A', 'type': 'string'}, - 'b': {'title': 'B', 'type': 'string', 'format': 'binary'}, - 'c': {'title': 'C', 'anyOf': [{'type': 'string'}, {'type': 'string', 'format': 'binary'}]}, - 'd': {'title': 'D', 'anyOf': [{'type': 'string'}, {'type': 'string', 'format': 'binary'}]}, - }, - 'required': ['c'], + 'properties': {'a': {'title': 'A', 'type': 'string', 'format': ''}}, + 'required': ['a'], } + base_schema['properties']['a']['format'] = expected_schema + + assert Model.schema() == base_schema -def test_str_constrained_types(): +@pytest.mark.parametrize( + 'field_type,expected_schema', + [ + (NoneStr, {'properties': {'a': {'title': 'A', 'type': 'string'}}}), + (NoneBytes, {'properties': {'a': {'title': 'A', 'type': 'string', 'format': 'binary'}}}), + ( + StrBytes, + { + 'properties': { + 'a': {'title': 'A', 'anyOf': [{'type': 'string'}, {'type': 'string', 'format': 'binary'}]} + }, + 'required': ['a'], + }, + ), + ( + NoneStrBytes, + { + 'properties': { + 'a': {'title': 'A', 'anyOf': [{'type': 'string'}, {'type': 'string', 'format': 'binary'}]} + } + }, + ), + ], +) +def test_str_basic_types(field_type, expected_schema): class Model(BaseModel): - a: StrictStr - b: ConstrainedStr - c: constr(min_length=3, max_length=5, regex='^text$') + a: field_type + + base_schema = {'title': 'Model', 'type': 'object'} + base_schema.update(expected_schema) + assert Model.schema() == base_schema + + +@pytest.mark.parametrize( + 'field_type,expected_schema', + [ + (StrictStr, {'title': 'A', 'type': 'string'}), + (ConstrainedStr, {'title': 'A', 'type': 'string'}), + ( + constr(min_length=3, max_length=5, regex='^text$'), + {'title': 'A', 'type': 'string', 'minLength': 3, 'maxLength': 5, 'pattern': '^text$'}, + ), + ], +) +def test_str_constrained_types(field_type, expected_schema): + class Model(BaseModel): + a: field_type - model_schema = Model.schema() - assert model_schema == { - 'title': 'Model', - 'type': 'object', - 'properties': { - 'a': {'title': 'A', 'type': 'string'}, - 'b': {'title': 'B', 'type': 'string'}, - 'c': {'title': 'C', 'type': 'string', 'minLength': 3, 'maxLength': 5, 'pattern': '^text$'}, - }, - 'required': ['a', 'b', 'c'], - } + base_schema = {'title': 'Model', 'type': 'object', 'properties': {'a': {}}, 'required': ['a']} + base_schema['properties']['a'] = expected_schema + + assert Model.schema() == base_schema -def test_special_str_types(): +@pytest.mark.parametrize( + 'field_type,expected_schema', + [ + (UrlStr, {'title': 'A', 'type': 'string', 'format': 'uri', 'minLength': 1, 'maxLength': 2 ** 16}), + ( + urlstr(min_length=5, max_length=10), + {'title': 'A', 'type': 'string', 'format': 'uri', 'minLength': 5, 'maxLength': 10}, + ), + (DSN, {'title': 'A', 'type': 'string', 'format': 'dsn'}), + ], +) +def test_special_str_types(field_type, expected_schema): class Model(BaseModel): - a: UrlStr - b: urlstr(min_length=5, max_length=10) - c: DSN + a: field_type - model_schema = Model.schema() - assert model_schema == { - 'title': 'Model', - 'type': 'object', - 'properties': { - 'a': {'title': 'A', 'type': 'string', 'format': 'uri', 'minLength': 1, 'maxLength': 2 ** 16}, - 'b': {'title': 'B', 'type': 'string', 'format': 'uri', 'minLength': 5, 'maxLength': 10}, - 'c': {'title': 'C', 'type': 'string', 'format': 'dsn'}, - }, - 'required': ['a', 'b', 'c'], - } + base_schema = {'title': 'Model', 'type': 'object', 'properties': {'a': {}}, 'required': ['a']} + base_schema['properties']['a'] = expected_schema + + assert Model.schema() == base_schema @pytest.mark.skipif(not email_validator, reason='email_validator not installed') -def test_email_str_types(): +@pytest.mark.parametrize('field_type,expected_schema', [(EmailStr, 'email'), (NameEmail, 'name-email')]) +def test_email_str_types(field_type, expected_schema): class Model(BaseModel): - a: EmailStr - b: NameEmail + a: field_type - model_schema = Model.schema() - assert model_schema == { + base_schema = { 'title': 'Model', 'type': 'object', - 'properties': { - 'a': {'title': 'A', 'type': 'string', 'format': 'email'}, - 'b': {'title': 'B', 'type': 'string', 'format': 'name-email'}, - }, - 'required': ['a', 'b'], + 'properties': {'a': {'title': 'A', 'type': 'string'}}, + 'required': ['a'], } + base_schema['properties']['a']['format'] = expected_schema + assert Model.schema() == base_schema -def test_special_int_types(): + +@pytest.mark.parametrize( + 'field_type,expected_schema', + [ + (ConstrainedInt, {}), + (conint(gt=5, lt=10), {'exclusiveMinimum': 5, 'exclusiveMaximum': 10}), + (conint(ge=5, le=10), {'minimum': 5, 'maximum': 10}), + (PositiveInt, {'exclusiveMinimum': 0}), + (NegativeInt, {'exclusiveMaximum': 0}), + ], +) +def test_special_int_types(field_type, expected_schema): class Model(BaseModel): - a: ConstrainedInt - b: conint(gt=5, lt=10) - c: conint(ge=5, le=10) - d: PositiveInt - e: NegativeInt + a: field_type - model_schema = Model.schema() - assert model_schema == { + base_schema = { 'title': 'Model', 'type': 'object', - 'properties': { - 'a': {'title': 'A', 'type': 'integer'}, - 'b': {'title': 'B', 'type': 'integer', 'exclusiveMinimum': 5, 'exclusiveMaximum': 10}, - 'c': {'title': 'C', 'type': 'integer', 'minimum': 5, 'maximum': 10}, - 'd': {'title': 'D', 'type': 'integer', 'exclusiveMinimum': 0}, - 'e': {'title': 'E', 'type': 'integer', 'exclusiveMaximum': 0}, - }, - 'required': ['a', 'b', 'c', 'd', 'e'], + 'properties': {'a': {'title': 'A', 'type': 'integer'}}, + 'required': ['a'], } - - -def test_special_float_types(): + base_schema['properties']['a'].update(expected_schema) + + assert Model.schema() == base_schema + + +@pytest.mark.parametrize( + 'field_type,expected_schema', + [ + (ConstrainedFloat, {}), + (confloat(gt=5, lt=10), {'exclusiveMinimum': 5, 'exclusiveMaximum': 10}), + (confloat(ge=5, le=10), {'minimum': 5, 'maximum': 10}), + (PositiveFloat, {'exclusiveMinimum': 0}), + (NegativeFloat, {'exclusiveMaximum': 0}), + (ConstrainedDecimal, {}), + (condecimal(gt=5, lt=10), {'exclusiveMinimum': 5, 'exclusiveMaximum': 10}), + (condecimal(ge=5, le=10), {'minimum': 5, 'maximum': 10}), + ], +) +def test_special_float_types(field_type, expected_schema): class Model(BaseModel): - a: ConstrainedFloat - b: confloat(gt=5, lt=10) - c: confloat(ge=5, le=10) - d: PositiveFloat - e: NegativeFloat - f: ConstrainedDecimal - g: condecimal(gt=5, lt=10) - h: condecimal(ge=5, le=10) + a: field_type - model_schema = Model.schema() - assert model_schema == { + base_schema = { 'title': 'Model', 'type': 'object', - 'properties': { - 'a': {'title': 'A', 'type': 'number'}, - 'b': {'title': 'B', 'type': 'number', 'exclusiveMinimum': 5, 'exclusiveMaximum': 10}, - 'c': {'title': 'C', 'type': 'number', 'minimum': 5, 'maximum': 10}, - 'd': {'title': 'D', 'type': 'number', 'exclusiveMinimum': 0}, - 'e': {'title': 'E', 'type': 'number', 'exclusiveMaximum': 0}, - 'f': {'title': 'F', 'type': 'number'}, - 'g': {'title': 'G', 'type': 'number', 'exclusiveMinimum': 5, 'exclusiveMaximum': 10}, - 'h': {'title': 'H', 'type': 'number', 'minimum': 5, 'maximum': 10}, - }, - 'required': ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], + 'properties': {'a': {'title': 'A', 'type': 'number'}}, + 'required': ['a'], } + base_schema['properties']['a'].update(expected_schema) + assert Model.schema() == base_schema -def test_uuid_types(): + +@pytest.mark.parametrize( + 'field_type,expected_schema', + [(UUID, 'uuid'), (UUID1, 'uuid1'), (UUID3, 'uuid3'), (UUID4, 'uuid4'), (UUID5, 'uuid5')], +) +def test_uuid_types(field_type, expected_schema): class Model(BaseModel): - a: UUID - b: UUID1 - c: UUID3 - d: UUID4 - e: UUID5 + a: field_type - model_schema = Model.schema() - assert model_schema == { + base_schema = { 'title': 'Model', 'type': 'object', - 'properties': { - 'a': {'title': 'A', 'type': 'string', 'format': 'uuid'}, - 'b': {'title': 'B', 'type': 'string', 'format': 'uuid1'}, - 'c': {'title': 'C', 'type': 'string', 'format': 'uuid3'}, - 'd': {'title': 'D', 'type': 'string', 'format': 'uuid4'}, - 'e': {'title': 'E', 'type': 'string', 'format': 'uuid5'}, - }, - 'required': ['a', 'b', 'c', 'd', 'e'], + 'properties': {'a': {'title': 'A', 'type': 'string', 'format': ''}}, + 'required': ['a'], } + base_schema['properties']['a']['format'] = expected_schema + assert Model.schema() == base_schema -def test_path_types(): + +@pytest.mark.parametrize( + 'field_type,expected_schema', [(FilePath, 'file-path'), (DirectoryPath, 'directory-path'), (Path, 'path')] +) +def test_path_types(field_type, expected_schema): class Model(BaseModel): - a: FilePath - b: DirectoryPath - c: Path + a: field_type - model_schema = Model.schema() - assert model_schema == { + base_schema = { 'title': 'Model', 'type': 'object', - 'properties': { - 'a': {'title': 'A', 'type': 'string', 'format': 'file-path'}, - 'b': {'title': 'B', 'type': 'string', 'format': 'directory-path'}, - 'c': {'title': 'C', 'type': 'string', 'format': 'path'}, - }, - 'required': ['a', 'b', 'c'], + 'properties': {'a': {'title': 'A', 'type': 'string', 'format': ''}}, + 'required': ['a'], } + base_schema['properties']['a']['format'] = expected_schema + + assert Model.schema() == base_schema def test_json_type(): @@ -599,9 +637,6 @@ class Foo(BaseModel): class Bar(BaseModel): b: Foo - class Baz(BaseModel): - c: Bar - class Ingredient(BaseModel): name: str @@ -609,8 +644,8 @@ class Pizza(BaseModel): name: str ingredients: List[Ingredient] - flat_models = get_flat_models_from_models([Baz, Pizza]) - assert flat_models == set([Foo, Bar, Baz, Ingredient, Pizza]) + flat_models = get_flat_models_from_models([Bar, Pizza]) + assert flat_models == set([Foo, Bar, Ingredient, Pizza]) def test_model_name_maps(): From d08eafbf191475e654c6d678d8b0e128a5b726c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 23:30:23 +0400 Subject: [PATCH 57/65] Fix formatting and imports I missed after rebase --- HISTORY.rst | 2 +- pydantic/fields.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index d48b2e5e28..f2e7deebef 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -4,7 +4,7 @@ History ------- v0.16.0 (2018-11-19) -.................. +.................... * refactor schema generation to be compatible with JSON Schema and OpenAPI specs, #308 by @tiangolo * add ``schema`` to ``schema`` module to generate top-level schemas from base models, #308 by @tiangolo diff --git a/pydantic/fields.py b/pydantic/fields.py index e8fe4fb712..29b4218aed 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -1,6 +1,6 @@ import inspect from enum import IntEnum -from typing import Any, Callable, List, Mapping, NamedTuple, Set, Tuple, Type, Union +from typing import Any, Callable, List, Mapping, NamedTuple, Pattern, Set, Tuple, Type, Union from . import errors as errors_ from .error_wrappers import ErrorWrapper @@ -153,7 +153,7 @@ def prepare(self): self._populate_sub_fields() self._populate_validators() - def _populate_sub_fields(self): + def _populate_sub_fields(self): # noqa: C901 (ignore complexity) # typing interface is horrible, we have to do some ugly checks if isinstance(self.type_, type) and issubclass(self.type_, JsonWrapper): self.type_ = self.type_.inner_type From 1870bd2afa84e271f5fee90ec53326dc6fabc028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 23:36:54 +0400 Subject: [PATCH 58/65] Fix new formatting errors from CI --- pydantic/schema.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index 183476e8d3..97aabb1514 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -47,7 +47,7 @@ def schema( else, e.g. for OpenAPI use ``#/components/schemas/``. The resulting generated schemas will still be at the top-level key ``definitions``, so you can extract them from there. But all the references will have the set prefix. - :return: dict with the JSON Schema with a ``definitions`` top-level key including the schema definitions for + :return: dict with the JSON Schema with a ``definitions`` top-level key including the schema definitions for the models and submodels passed in ``models``. """ ref_prefix = ref_prefix or default_prefix @@ -73,7 +73,7 @@ def schema( def model_schema(model: 'main.BaseModel', by_alias=True, ref_prefix=None) -> Dict[str, Any]: """ - Generate a JSON Schema for one model. With all the submodels defined in the ``definitions`` top-level + Generate a JSON Schema for one model. With all the submodels defined in the ``definitions`` top-level JSON key. :param model: a Pydantic model (a class that inherits from BaseModel) @@ -108,7 +108,7 @@ def field_schema( :param field: a Pydantic Field :param by_alias: use the defined alias (if any) in the returned schema :param model_name_map: used to generate the JSON Schema references to other models included in the definitions - :param ref_prefix: the JSON Pointer prefix to use for references to other schemas, if None, the default of + :param ref_prefix: the JSON Pointer prefix to use for references to other schemas, if None, the default of #/definitions/ will be used :return: tuple of the schema for this field and additional definitions """ @@ -148,8 +148,8 @@ def get_model_name_map( ) -> Tuple[Dict[str, Type['main.BaseModel']], Dict[Type['main.BaseModel'], str]]: """ Process a set of models and generate unique names for them to be used as keys in the JSON Schema - definitions. By default the names are the same class name. But if two models in diferent Python - modules have the same name (e.g. "users.Model" and "items.Model"), the generated names will be + definitions. By default the names are the same class name. But if two models in diferent Python + modules have the same name (e.g. "users.Model" and "items.Model"), the generated names will be based on the Python module path for those conflicting models to prevent name collisions. :param unique_models: a Python set of models From 66d9f2304ce689bdb8469a7ff91c01ebba671d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Nov 2018 23:40:45 +0400 Subject: [PATCH 59/65] Re-trigger Travis CI, Python 3.7-dev random error again, no re-run click in Travis for non owners From b11196d7347a7c517b07c3b5656aab5fe86bd8c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 21 Nov 2018 17:41:29 +0400 Subject: [PATCH 60/65] Trigger annotation error with non-forward references --- pydantic/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index 97aabb1514..a249ace3bd 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -32,7 +32,7 @@ def schema( - models: Sequence[Type['main.BaseModel']], *, by_alias=True, title=None, description=None, ref_prefix=None + models: Sequence[Type[main.BaseModel]], *, by_alias=True, title=None, description=None, ref_prefix=None ) -> Dict: """ Process a list of models and generate a single JSON Schema with all of them defined in the ``definitions`` From b714acc915e0700debe89b2411caa820fb75b1bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 21 Nov 2018 18:47:07 +0400 Subject: [PATCH 61/65] Add docstrings for submodel schema --- pydantic/schema.py | 102 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 93 insertions(+), 9 deletions(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index a249ace3bd..652dadcf85 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -32,7 +32,7 @@ def schema( - models: Sequence[Type[main.BaseModel]], *, by_alias=True, title=None, description=None, ref_prefix=None + models: Sequence[Type['main.BaseModel']], *, by_alias=True, title=None, description=None, ref_prefix=None ) -> Dict: """ Process a list of models and generate a single JSON Schema with all of them defined in the ``definitions`` @@ -105,7 +105,7 @@ def field_schema( is a model and has submodels, and those submodels don't have overrides (as ``title``, ``default``, etc), they will be included in the definitions and referenced in the schema instead of included recursively. - :param field: a Pydantic Field + :param field: a Pydantic ``Field`` :param by_alias: use the defined alias (if any) in the returned schema :param model_name_map: used to generate the JSON Schema references to other models included in the definitions :param ref_prefix: the JSON Pointer prefix to use for references to other schemas, if None, the default of @@ -174,6 +174,15 @@ def get_model_name_map( def get_flat_models_from_model(model: Type['main.BaseModel']) -> Set[Type['main.BaseModel']]: + """ + Take a single ``model`` and generate a set with itself and all the submodels in the tree. I.e. if you pass + model ``Foo`` (subclass of Pydantic ``BaseModel``) as ``model``, and it has a field of type ``Bar`` (also + subclass of ``BaseModel``) and that model ``Bar`` has a field of type ``Baz`` (also subclass of ``BaseModel``), + the return value will be ``set([Foo, Bar, Baz])``. + + :param model: a Pydantic ``BaseModel`` subclass + :return: a set with the initial model and all its submodels + """ flat_models = set() flat_models.add(model) flat_models |= get_flat_models_from_fields(model.__fields__.values()) @@ -181,6 +190,16 @@ def get_flat_models_from_model(model: Type['main.BaseModel']) -> Set[Type['main. def get_flat_models_from_field(field: Field) -> Set[Type['main.BaseModel']]: + """ + Take a single Pydantic ``Field`` (from a model) that could have been declared as a sublcass of BaseModel + (so, it could be a submodel), and generate a set with its model and all the submodels in the tree. + I.e. if you pass a field that was declared to be of type ``Foo`` (subclass of BaseModel) as ``field``, and that + model ``Foo`` has a field of type ``Bar`` (also subclass of ``BaseModel``) and that model ``Bar`` has a field of + type ``Baz`` (also subclass of ``BaseModel``), the return value will be ``set([Foo, Bar, Baz])``. + + :param field: a Pydantic ``Field`` + :return: a set with the model used in the declaration for this field, if any, and all its submodels + """ flat_models = set() if field.sub_fields: flat_models |= get_flat_models_from_fields(field.sub_fields) @@ -190,6 +209,16 @@ def get_flat_models_from_field(field: Field) -> Set[Type['main.BaseModel']]: def get_flat_models_from_fields(fields) -> Set[Type['main.BaseModel']]: + """ + Take a list of Pydantic ``Field``s (from a model) that could have been declared as sublcasses of ``BaseModel`` + (so, any of them could be a submodel), and generate a set with their models and all the submodels in the tree. + I.e. if you pass a the fields of a model ``Foo`` (subclass of ``BaseModel``) as ``fields``, and on of them has a + field of type ``Bar`` (also subclass of ``BaseModel``) and that model ``Bar`` has a field of type ``Baz`` (also + subclass of ``BaseModel``), the return value will be ``set([Foo, Bar, Baz])``. + + :param fields: a list of Pydantic ``Field``s + :return: a set with any model declared in the fields, and all their submodels + """ flat_models = set() for field in fields: flat_models |= get_flat_models_from_field(field) @@ -197,6 +226,14 @@ def get_flat_models_from_fields(fields) -> Set[Type['main.BaseModel']]: def get_flat_models_from_models(models: Sequence[Type['main.BaseModel']]) -> Set[Type['main.BaseModel']]: + """ + Take a list of ``models`` and generate a set with them and all their submodels in their trees. I.e. if you pass + a list of two models, ``Foo`` and ``Bar``, both subclasses of Pydantic ``BaseModel`` as models, and ``Bar`` has + a field of type ``Baz`` (also subclass of ``BaseModel``), the return value will be ``set([Foo, Bar, Baz])``. + + :param model: a Pydantic ``BaseModel`` subclass + :return: a set with the initial model and all its submodels + """ flat_models = set() for model in models: flat_models |= get_flat_models_from_model(model) @@ -215,6 +252,15 @@ def field_type_schema( schema_overrides=False, ref_prefix=None, ) -> Tuple[Dict[str, Any], Dict[str, Any]]: + """ + Used by ``field_schema()``, you probably should be using that function. + + Take a single ``field`` and generate the schema for its type only, not including additional + information as title, etc. Also return additional schema definitions, from submodels. + + :param model: a Pydantic ``Field`` subclass + :return: tuple of the type schema for this field and additional definitions form submodules + """ definitions = {} ref_prefix = ref_prefix or default_prefix if field.shape is Shape.LIST: @@ -265,27 +311,46 @@ def field_type_schema( def model_process_schema( - class_: Type['main.BaseModel'], *, by_alias=True, model_name_map: Dict[Type['main.BaseModel'], str], ref_prefix=None + model: Type['main.BaseModel'], *, by_alias=True, model_name_map: Dict[Type['main.BaseModel'], str], ref_prefix=None ) -> Tuple[Dict[str, Any], Dict[str, Any]]: + """ + Used by ``model_schema()``, you probably should be using that function. + + Take a single ``model`` and generate its schema. Also return additional schema definitions, from submodels. The + submodels of the returned schema will be referenced, but their definitions will not be included in the schema. All + the definitions are returned as the second value. + + :param model: a Pydantic ``BaseModel`` subclass + :return: tuple of the schema for this model and additional definitions form submodules + """ ref_prefix = ref_prefix or default_prefix - s = {'title': class_.__config__.title or class_.__name__} - if class_.__doc__: - s['description'] = clean_docstring(class_.__doc__) + s = {'title': model.__config__.title or model.__name__} + if model.__doc__: + s['description'] = clean_docstring(model.__doc__) m_schema, m_definitions = model_type_schema( - class_, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix + model, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix ) s.update(m_schema) return s, m_definitions def model_type_schema( - class_: 'main.BaseModel', *, by_alias: bool, model_name_map: Dict[Type['main.BaseModel'], str], ref_prefix=None + model: 'main.BaseModel', *, by_alias: bool, model_name_map: Dict[Type['main.BaseModel'], str], ref_prefix=None ): + """ + You probably should be using ``model_schema()``, this function is indirectly used by that function. + + Take a single ``model`` and generate the schema for its type only, not including additional + information as title, etc. Also return additional schema definitions, from submodels. + + :param model: a Pydantic ``BaseModel`` subclass + :return: tuple of the type schema for this model and additional definitions form submodules + """ ref_prefix = ref_prefix or default_prefix properties = {} required = [] definitions = {} - for k, f in class_.__fields__.items(): + for k, f in model.__fields__.items(): f_schema, f_definitions = field_schema( f, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix ) @@ -312,6 +377,16 @@ def field_singleton_sub_fields_schema( schema_overrides=False, ref_prefix=None, ) -> Tuple[Dict[str, Any], Dict[str, Any]]: + """ + This function is indirectly used by ``field_schema()``, you probably should be using that function. + + Take a list of Pydantic ``Field`` from the declaration of a type with parameters, and generate their + schema. I.e., fields used as "type parameters", like ``str`` and ``int`` in ``Tuple[str, int]``. + + :param sub_fields: a list of Pydantic ``Field``s from a declaration using a type with parameters + :return: a tuple with the schema for the type declared with parameters and additional definitions from + any submodules + """ ref_prefix = ref_prefix or default_prefix definitions = {} if len(sub_fields) == 1: @@ -388,6 +463,15 @@ def field_singleton_schema( # noqa: C901 (ignore complexity) schema_overrides=False, ref_prefix=None, ) -> Tuple[Dict[str, Any], Dict[str, Any]]: + """ + This function is indirectly used by ``field_schema()``, you probably should be using that function. + + Take a single Pydantic ``Field``, and return its schema and any additional definitions from submodels. + + :param field: a single Pydantic ``Field`` + :return: a tuple with the schema for the type declared and additional definitions from any submodules + """ + ref_prefix = ref_prefix or default_prefix definitions = {} if field.sub_fields: From 1604f7212780b87d05b8c5ea48b131f3905fb853 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Thu, 22 Nov 2018 14:16:36 +0000 Subject: [PATCH 62/65] tweaks and rewrite schema mapping table in python --- .gitignore | 1 + HISTORY.rst | 2 +- docs/Makefile | 1 + docs/index.rst | 139 +++------------ docs/schema_mapping.py | 385 +++++++++++++++++++++++++++++++++++++++++ pydantic/schema.py | 80 ++++----- 6 files changed, 445 insertions(+), 163 deletions(-) create mode 100755 docs/schema_mapping.py diff --git a/.gitignore b/.gitignore index 7eec19d361..d27f0b95c6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ htmlcov/ benchmarks/*.json docs/_build/ docs/.TMP_HISTORY.rst +docs/.tmp_schema_mappings.rst .pytest_cache/ .vscode/ _build/ diff --git a/HISTORY.rst b/HISTORY.rst index f2e7deebef..ea982f912e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,7 +3,7 @@ History ------- -v0.16.0 (2018-11-19) +v0.16.0 (2018-XX-XX) .................... * refactor schema generation to be compatible with JSON Schema and OpenAPI specs, #308 by @tiangolo diff --git a/docs/Makefile b/docs/Makefile index 946ca0d121..86928e9389 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -14,6 +14,7 @@ clean: .PHONY: html html: clean + ./schema_mapping.py mkdir -p $(STATICDIR) $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo diff --git a/docs/index.rst b/docs/index.rst index cecd171156..85af3a6670 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -223,21 +223,26 @@ Schema Creation .. literalinclude:: examples/schema1.py +(This script is complete, it should run "as is") + Outputs: .. literalinclude:: examples/schema1.json -(This script is complete, it should run "as is") - -The generated schemas are compliant with the specifications: `JSON Schema Core `__, `JSON Schema Validation `__ and `OpenAPI `__. +The generated schemas are compliant with the specifications: +`JSON Schema Core `__, +`JSON Schema Validation `__ and `OpenAPI `__. -``BaseModel.schema`` will return a dict of the schema, while ``BaseModel.schema_json`` will return a JSON string representation of that. +``BaseModel.schema`` will return a dict of the schema, while ``BaseModel.schema_json`` will return a JSON string +representation of that. -"submodels" used are added to the ``definitons`` JSON attribute and referenced, as per the spec. +Sub-models used are added to the ``definitions`` JSON attribute and referenced, as per the spec. -All submodels (and their submodels) schemas are put directly in a top-level ``definitions`` JSON key for easy re-use and reference. +All sub-models (and their sub-models) schemas are put directly in a top-level ``definitions`` JSON key for easy re-use +and reference. -"submodels" with modifications via the (``Schema`` class) like a custom title, description or default value, are recursively included instead of referenced. +"sub-models" with modifications (via the ``Schema`` class) like a custom title, description or default value, +are recursively included instead of referenced. The ``description`` for models is taken from the docstring of the class. @@ -255,129 +260,41 @@ to set all the arguments above except ``default``. The schema is generated by default using aliases as keys, it can also be generated using model property names not aliases with ``MainModel.schema/schema_json(by_alias=False)``. -Types, custom field types, and constraints (as ``max_length``) are mapped to the corresponding `JSON Schema Core `__ spec format when there's an equivalent available, next to `JSON Schema Validation `__, `OpenAPI Data Types `__ (which are based on JSON Schema), or otherwise use the standard ``format`` JSON field to define Pydantic extensions for more complex ``string`` sub-types. +Types, custom field types, and constraints (as ``max_length``) are mapped to the corresponding +`JSON Schema Core `__ spec format when there's +an equivalent available, next to `JSON Schema Validation `__, +`OpenAPI Data Types `__ +(which are based on JSON Schema), or otherwise use the standard ``format`` JSON field to define Pydantic extensions +for more complex ``string`` sub-types. The field schema mapping from Python / Pydantic to JSON Schema is done as follows: -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| Python type | JSON Schema Type | Additional JSON Schema | Defined in | Notes | -+=============================================================+==================+==================================================================================+======================================+===================================================================================================================================================================================================================================+ -| ``bool`` | ``boolean`` | | JSON Schema Core | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``str`` | ``string`` | | JSON Schema Core | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``float`` | ``number`` | | JSON Schema Core | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``int`` | ``integer`` | | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``dict`` | ``object`` | | JSON Schema Core | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``list`` | ``array`` | | JSON Schema Core | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``tuple`` | ``array`` | | JSON Schema Core | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``set`` | ``array`` | ``{"uniqueItems": true}`` | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``List[str]`` | ``array`` | ``{"items": {"type": "string"}}`` | JSON Schema Validation | And equivalently for any other sub type, e.g. List[int]. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``Tuple[str, int]`` | ``array`` | ``{"items": [{"type": "string"}, {"type": "integer"}]}`` | JSON Schema Validation | And equivalently for any other set of subtypes. Note: If using schemas for OpenAPI, you shouldn't use this declaration, as it would not be valid in OpenAPI (although it is valid in JSON Schema). | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``Dict[str, int]`` | ``object`` | ``{"additionalProperties": {"type": "integer"}}`` | JSON Schema Validation | And equivalently for any other subfields for dicts. Have in mind that although you can use other types as keys for dicts with Pydantic, only strings are valid keys for JSON, and so, only str is valid as JSON Schema key types. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``Union[str, int]`` | * ``anyOf`` | ``{"anyOf": [{"type": "string"}, {"type": "integer"}]}`` | JSON Schema Validation | And equivalently for any other subfields for unions. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``Enum`` | * ``enum`` | ``{"enum": [...]}`` | JSON Schema Validation | All the literal values in the enum are included in the definition. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``EmailStr`` | ``string`` | ``{"format": "email"}`` | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``NameEmail`` | ``string`` | ``{"format": "name-email"}`` | Pydantic standard "format" extension | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``UrlStr`` | ``string`` | ``{"format": "uri"}`` | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``DSN`` | ``string`` | ``{"format": "dsn"}`` | Pydantic standard "format" extension | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``bytes`` | ``string`` | ``{"format": "binary"}`` | OpenAPI | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``Decimal`` | ``number`` | | JSON Schema Core | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``UUID1`` | ``string`` | ``{"format": "uuid1"}`` | Pydantic standard "format" extension | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``UUID3`` | ``string`` | ``{"format": "uuid3"}`` | Pydantic standard "format" extension | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``UUID4`` | ``string`` | ``{"format": "uuid4"}`` | Pydantic standard "format" extension | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``UUID5`` | ``string`` | ``{"format": "uuid5"}`` | Pydantic standard "format" extension | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``UUID`` | ``string`` | ``{"format": "uuid"}`` | Pydantic standard "format" extension | Suggested in OpenAPI. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``FilePath`` | ``string`` | ``{"format": "file-path"}`` | Pydantic standard "format" extension | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``DirectoryPath`` | ``string`` | ``{"format": "directory-path"}`` | Pydantic standard "format" extension | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``Path`` | ``string`` | ``{"format": "path"}`` | Pydantic standard "format" extension | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``datetime`` | ``string`` | ``{"format": "date-time"}`` | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``date`` | ``string`` | ``{"format": "date"}`` | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``time`` | ``string`` | ``{"format": "time"}`` | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``timedelta`` | ``string`` | ``{"format": "time-delta"}`` | Pydantic standard "format" extension | Suggested in JSON Schema repository's issues by maintainer. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``Json`` | ``string`` | ``{"format": "json-string"}`` | Pydantic standard "format" extension | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``StrictStr`` | ``string`` | | JSON Schema Core | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``ConstrainedStr`` | ``string`` | | JSON Schema Core | If the type has values declared for the constraints, they are included as validations. See the mapping for ``constr`` below. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``constr(regex='^text$', min_length=2, max_length=10)`` | ``string`` | ``{"pattern": "^text$", "minLength": 2, "maxLength": 10}`` | JSON Schema Validation | Any argument not passed to the function (not defined) will not be included in the schema. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``ConstrainedInt`` | ``integer`` | | JSON Schema Core | If the type has values declared for the constraints, they are included as validations. See the mapping for ``conint`` below. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``conint(gt=1, ge=2, lt=6, le=5)`` | ``integer`` | ``{"maximum": 5, "exclusiveMaximum": 6, "minimum": 2, "exclusiveMinimum": 1}`` | | Any argument not passed to the function (not defined) will not be included in the schema. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``PositiveInt`` | ``integer`` | ``{"exclusiveMinimum": 0}`` | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``NegativeInt`` | ``integer`` | ``{"exclusiveMaximum": 0}`` | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``ConstrainedFloat`` | ``number`` | | JSON Schema Core | If the type has values declared for the constraints, they are included as validations. See the mapping for ``confloat`` below. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``confloat(gt=1, ge=2, lt=6, le=5)`` | ``number`` | ``{"maximum": 5, "exclusiveMaximum": 6, "minimum": 2, "exclusiveMinimum": 1}`` | JSON Schema Validation | Any argument not passed to the function (not defined) will not be included in the schema. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``PositiveFloat`` | ``number`` | ``{"exclusiveMinimum": 0}`` | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``NegativeFloat`` | ``number`` | ``{"exclusiveMaximum": 0}`` | JSON Schema Validation | | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``ConstrainedDecimal`` | ``number`` | | JSON Schema Core | If the type has values declared for the constraints, they are included as validations. See the mapping for ``condecimal`` below. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``condecimal(gt=1, ge=2, lt=6, le=5)`` | ``number`` | ``{"maximum": 5, "exclusiveMaximum": 6, "minimum": 2, "exclusiveMinimum": 1}`` | JSON Schema Validation | Any argument not passed to the function (not defined) will not be included in the schema. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``BaseModel`` | ``object`` | | JSON Schema Core | All the properties defined will be defined with standard JSON Schema, including submodels. | -+-------------------------------------------------------------+------------------+----------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - - -You can also generate a top-level JSON Schema that only includes a list of models and all their related submodules in its ``definitions``: +.. include:: .tmp_schema_mappings.rst + + +You can also generate a top-level JSON Schema that only includes a list of models and all their related +submodules in its ``definitions``: .. literalinclude:: examples/schema2.py +(This script is complete, it should run "as is") + Outputs: .. literalinclude:: examples/schema2.json -(This script is complete, it should run "as is") - -You can customize the generated ``$ref`` JSON location, the definitions will still be in the key ``definitions`` and you can still get them from there, but the references will point to your defined prefix instead of the default. +You can customize the generated ``$ref`` JSON location, the definitions will still be in the key ``definitions`` and you can still get them from there, but the references will point to your defined prefix instead of the default. This is useful if you need to extend or modify JSON Schema default definitions location, e.g. with OpenAPI: .. literalinclude:: examples/schema3.py +(This script is complete, it should run "as is") + Outputs: .. literalinclude:: examples/schema3.json -(This script is complete, it should run "as is") - Error Handling .............. diff --git a/docs/schema_mapping.py b/docs/schema_mapping.py new file mode 100755 index 0000000000..b7b16f6cbb --- /dev/null +++ b/docs/schema_mapping.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python3 +""" +Build a table of Python / Pydantic to JSON Schema mappings. + +Done like this rather than as a raw rst table to make future edits easier. + +Please edit this file directly not .tmp_schema_mappings.rst +""" + +table = [ + [ + 'bool', + 'boolean', + '', + 'JSON Schema Core', + '' + ], + [ + 'str', + 'string', + '', + 'JSON Schema Core', + '' + ], + [ + 'float', + 'number', + '', + 'JSON Schema Core', + '' + ], + [ + 'int', + 'integer', + '', + 'JSON Schema Validation', + '' + ], + [ + 'dict', + 'object', + '', + 'JSON Schema Core', + '' + ], + [ + 'list', + 'array', + '', + 'JSON Schema Core', + '' + ], + [ + 'tuple', + 'array', + '', + 'JSON Schema Core', + '' + ], + [ + 'set', + 'array', + '{"uniqueItems": true}', + 'JSON Schema Validation', + '' + ], + [ + 'List[str]', + 'array', + '{"items": {"type": "string"}}', + 'JSON Schema Validation', + 'And equivalently for any other sub type, e.g. List[int].' + ], + [ + 'Tuple[str, int]', + 'array', + '{"items": [{"type": "string"}, {"type": "integer"}]}', + 'JSON Schema Validation', + ( + 'And equivalently for any other set of subtypes. Note: If using schemas for OpenAPI, ' + 'you shouldn\'t use this declaration, as it would not be valid in OpenAPI (although it is ' + 'valid in JSON Schema).' + ) + ], + [ + 'Dict[str, int]', + 'object', + '{"additionalProperties": {"type": "integer"}}', + 'JSON Schema Validation', + ( + 'And equivalently for any other subfields for dicts. Have in mind that although you can use other types as ' + 'keys for dicts with Pydantic, only strings are valid keys for JSON, and so, only str is valid as ' + 'JSON Schema key types.' + ) + ], + [ + 'Union[str, int]', + 'anyOf', + '{"anyOf": [{"type": "string"}, {"type": "integer"}]}', + 'JSON Schema Validation', + 'And equivalently for any other subfields for unions.' + ], + [ + 'Enum', + 'enum', + '{"enum": [...]}', + 'JSON Schema Validation', + 'All the literal values in the enum are included in the definition.' + ], + [ + 'EmailStr', + 'string', + '{"format": "email"}', + 'JSON Schema Validation', + '' + ], + [ + 'NameEmail', + 'string', + '{"format": "name-email"}', + 'Pydantic standard "format" extension', + '' + ], + [ + 'UrlStr', + 'string', + '{"format": "uri"}', + 'JSON Schema Validation', + '' + ], + [ + 'DSN', + 'string', + '{"format": "dsn"}', + 'Pydantic standard "format" extension', + '' + ], + [ + 'bytes', + 'string', + '{"format": "binary"}', + 'OpenAPI', + '' + ], + [ + 'Decimal', + 'number', + '', + 'JSON Schema Core', + '' + ], + [ + 'UUID1', + 'string', + '{"format": "uuid1"}', + 'Pydantic standard "format" extension', + '' + ], + [ + 'UUID3', + 'string', + '{"format": "uuid3"}', + 'Pydantic standard "format" extension', + '' + ], + [ + 'UUID4', + 'string', + '{"format": "uuid4"}', + 'Pydantic standard "format" extension', + '' + ], + [ + 'UUID5', + 'string', + '{"format": "uuid5"}', + 'Pydantic standard "format" extension', + '' + ], + [ + 'UUID', + 'string', + '{"format": "uuid"}', + 'Pydantic standard "format" extension', + 'Suggested in OpenAPI.' + ], + [ + 'FilePath', + 'string', + '{"format": "file-path"}', + 'Pydantic standard "format" extension', + '' + ], + [ + 'DirectoryPath', + 'string', + '{"format": "directory-path"}', + 'Pydantic standard "format" extension', + '' + ], + [ + 'Path', + 'string', + '{"format": "path"}', + 'Pydantic standard "format" extension', + '' + ], + [ + 'datetime', + 'string', + '{"format": "date-time"}', + 'JSON Schema Validation', + '' + ], + [ + 'date', + 'string', + '{"format": "date"}', + 'JSON Schema Validation', + '' + ], + [ + 'time', + 'string', + '{"format": "time"}', + 'JSON Schema Validation', + '' + ], + [ + 'timedelta', + 'string', + '{"format": "time-delta"}', + 'Pydantic standard "format" extension', + 'Suggested in JSON Schema repository\'s issues by maintainer.' + ], + [ + 'Json', + 'string', + '{"format": "json-string"}', + 'Pydantic standard "format" extension', + '' + ], + [ + 'StrictStr', + 'string', + '', + 'JSON Schema Core', + '' + ], + [ + 'ConstrainedStr', + 'string', + '', + 'JSON Schema Core', + ( + 'If the type has values declared for the constraints, they are included as validations. ' + 'See the mapping for ``constr`` below.' + ) + ], + [ + 'constr(regex=\'^text$\', min_length=2, max_length=10)', + 'string', + '{"pattern": "^text$", "minLength": 2, "maxLength": 10}', + 'JSON Schema Validation', + 'Any argument not passed to the function (not defined) will not be included in the schema.' + ], + [ + 'ConstrainedInt', + 'integer', + '', + 'JSON Schema Core', + ( + 'If the type has values declared for the constraints, they are included as validations. ' + 'See the mapping for ``conint`` below.' + ) + ], + [ + 'conint(gt=1, ge=2, lt=6, le=5)', + 'integer', + '{"maximum": 5, "exclusiveMaximum": 6, "minimum": 2, "exclusiveMinimum": 1}', + '', + 'Any argument not passed to the function (not defined) will not be included in the schema.' + ], + [ + 'PositiveInt', + 'integer', + '{"exclusiveMinimum": 0}', + 'JSON Schema Validation', + '' + ], + [ + 'NegativeInt', + 'integer', + '{"exclusiveMaximum": 0}', + 'JSON Schema Validation', + '' + ], + [ + 'ConstrainedFloat', + 'number', + '', + 'JSON Schema Core', + ( + 'If the type has values declared for the constraints, they are included as validations.' + 'See the mapping for ``confloat`` below.' + ) + ], + [ + 'confloat(gt=1, ge=2, lt=6, le=5)', + 'number', + '{"maximum": 5, "exclusiveMaximum": 6, "minimum": 2, "exclusiveMinimum": 1}', + 'JSON Schema Validation', + 'Any argument not passed to the function (not defined) will not be included in the schema.' + ], + [ + 'PositiveFloat', + 'number', + '{"exclusiveMinimum": 0}', + 'JSON Schema Validation', + '' + ], + [ + 'NegativeFloat', + 'number', + '{"exclusiveMaximum": 0}', + 'JSON Schema Validation', + '' + ], + [ + 'ConstrainedDecimal', + 'number', + '', + 'JSON Schema Core', + ( + 'If the type has values declared for the constraints, they are included as validations. ' + 'See the mapping for ``condecimal`` below.' + ) + ], + [ + 'condecimal(gt=1, ge=2, lt=6, le=5)', + 'number', + '{"maximum": 5, "exclusiveMaximum": 6, "minimum": 2, "exclusiveMinimum": 1}', + 'JSON Schema Validation', + 'Any argument not passed to the function (not defined) will not be included in the schema.' + ], + [ + 'BaseModel', + 'object', + '', + 'JSON Schema Core', + 'All the properties defined will be defined with standard JSON Schema, including submodels.' + ] +] + +headings = [ + 'Python type', + 'JSON Schema Type', + 'Additional JSON Schema', + 'Defined in', + 'Notes', +] + +v = '' +col_width = 300 +for _ in range(5): + v += '+' + '-' * col_width +v += '+\n|' +for heading in headings: + v += f' {heading:{col_width - 2}} |' +v += '\n' +for _ in range(5): + v += '+' + '=' * col_width +v += '+' +for row in table: + v += '\n|' + for i, text in enumerate(row): + text = f'``{text}``' if i < 3 and text else text + v += f' {text:{col_width - 2}} |' + v += '\n' + for _ in range(5): + v += '+' + '-' * col_width + v += '+' + +with open('.tmp_schema_mappings.rst', 'w') as f: + f.write(v) diff --git a/pydantic/schema.py b/pydantic/schema.py index 652dadcf85..07cb3e860f 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -36,19 +36,19 @@ def schema( ) -> Dict: """ Process a list of models and generate a single JSON Schema with all of them defined in the ``definitions`` - top-level JSON key, including their submodels. + top-level JSON key, including their sub-models. :param models: a list of models to include in the generated JSON Schema :param by_alias: generate the schemas using the aliases defined, if any :param title: title for the generated schema that includes the definitions :param description: description for the generated schema :param ref_prefix: the JSON Pointer prefix for schema references with ``$ref``, if None, will be set to the - default of ``#/definitions/``. Update it if you want the schemas to reference the definitions somewhere - else, e.g. for OpenAPI use ``#/components/schemas/``. The resulting generated schemas will still be at the - top-level key ``definitions``, so you can extract them from there. But all the references will have the set - prefix. + default of ``#/definitions/``. Update it if you want the schemas to reference the definitions somewhere + else, e.g. for OpenAPI use ``#/components/schemas/``. The resulting generated schemas will still be at the + top-level key ``definitions``, so you can extract them from there. But all the references will have the set + prefix. :return: dict with the JSON Schema with a ``definitions`` top-level key including the schema definitions for - the models and submodels passed in ``models``. + the models and sub-models passed in ``models``. """ ref_prefix = ref_prefix or default_prefix flat_models = get_flat_models_from_models(models) @@ -71,18 +71,18 @@ def schema( return output_schema -def model_schema(model: 'main.BaseModel', by_alias=True, ref_prefix=None) -> Dict[str, Any]: +def model_schema(model: Type['main.BaseModel'], by_alias=True, ref_prefix=None) -> Dict[str, Any]: """ - Generate a JSON Schema for one model. With all the submodels defined in the ``definitions`` top-level + Generate a JSON Schema for one model. With all the sub-models defined in the ``definitions`` top-level JSON key. :param model: a Pydantic model (a class that inherits from BaseModel) :param by_alias: generate the schemas using the aliases defined, if any :param ref_prefix: the JSON Pointer prefix for schema references with ``$ref``, if None, will be set to the - default of ``#/definitions/``. Update it if you want the schemas to reference the definitions somewhere - else, e.g. for OpenAPI use ``#/components/schemas/``. The resulting generated schemas will still be at the - top-level key ``definitions``, so you can extract them from there. But all the references will have the set - prefix. + default of ``#/definitions/``. Update it if you want the schemas to reference the definitions somewhere + else, e.g. for OpenAPI use ``#/components/schemas/``. The resulting generated schemas will still be at the + top-level key ``definitions``, so you can extract them from there. But all the references will have the set + prefix. :return: dict with the JSON Schema for the passed ``model`` """ ref_prefix = ref_prefix or default_prefix @@ -102,14 +102,14 @@ def field_schema( """ Process a Pydantic field and return a tuple with a JSON Schema for it as the first item. Also return a dictionary of definitions with models as keys and their schemas as values. If the passed field - is a model and has submodels, and those submodels don't have overrides (as ``title``, ``default``, etc), they + is a model and has sub-models, and those sub-models don't have overrides (as ``title``, ``default``, etc), they will be included in the definitions and referenced in the schema instead of included recursively. :param field: a Pydantic ``Field`` :param by_alias: use the defined alias (if any) in the returned schema :param model_name_map: used to generate the JSON Schema references to other models included in the definitions :param ref_prefix: the JSON Pointer prefix to use for references to other schemas, if None, the default of - #/definitions/ will be used + #/definitions/ will be used :return: tuple of the schema for this field and additional definitions """ ref_prefix = ref_prefix or default_prefix @@ -143,12 +143,10 @@ def field_schema( return s, f_definitions -def get_model_name_map( - unique_models: Set[Type['main.BaseModel']] -) -> Tuple[Dict[str, Type['main.BaseModel']], Dict[Type['main.BaseModel'], str]]: +def get_model_name_map(unique_models: Set[Type['main.BaseModel']]) -> Dict[Type['main.BaseModel'], str]: """ Process a set of models and generate unique names for them to be used as keys in the JSON Schema - definitions. By default the names are the same class name. But if two models in diferent Python + definitions. By default the names are the same as the class name. But if two models in different Python modules have the same name (e.g. "users.Model" and "items.Model"), the generated names will be based on the Python module path for those conflicting models to prevent name collisions. @@ -169,19 +167,18 @@ def get_model_name_map( name_model_map[get_long_model_name(model)] = model else: name_model_map[model_name] = model - model_name_map = {v: k for k, v in name_model_map.items()} - return model_name_map + return {v: k for k, v in name_model_map.items()} def get_flat_models_from_model(model: Type['main.BaseModel']) -> Set[Type['main.BaseModel']]: """ - Take a single ``model`` and generate a set with itself and all the submodels in the tree. I.e. if you pass + Take a single ``model`` and generate a set with itself and all the sub-models in the tree. I.e. if you pass model ``Foo`` (subclass of Pydantic ``BaseModel``) as ``model``, and it has a field of type ``Bar`` (also subclass of ``BaseModel``) and that model ``Bar`` has a field of type ``Baz`` (also subclass of ``BaseModel``), the return value will be ``set([Foo, Bar, Baz])``. :param model: a Pydantic ``BaseModel`` subclass - :return: a set with the initial model and all its submodels + :return: a set with the initial model and all its sub-models """ flat_models = set() flat_models.add(model) @@ -192,13 +189,13 @@ def get_flat_models_from_model(model: Type['main.BaseModel']) -> Set[Type['main. def get_flat_models_from_field(field: Field) -> Set[Type['main.BaseModel']]: """ Take a single Pydantic ``Field`` (from a model) that could have been declared as a sublcass of BaseModel - (so, it could be a submodel), and generate a set with its model and all the submodels in the tree. + (so, it could be a submodel), and generate a set with its model and all the sub-models in the tree. I.e. if you pass a field that was declared to be of type ``Foo`` (subclass of BaseModel) as ``field``, and that model ``Foo`` has a field of type ``Bar`` (also subclass of ``BaseModel``) and that model ``Bar`` has a field of type ``Baz`` (also subclass of ``BaseModel``), the return value will be ``set([Foo, Bar, Baz])``. :param field: a Pydantic ``Field`` - :return: a set with the model used in the declaration for this field, if any, and all its submodels + :return: a set with the model used in the declaration for this field, if any, and all its sub-models """ flat_models = set() if field.sub_fields: @@ -211,13 +208,13 @@ def get_flat_models_from_field(field: Field) -> Set[Type['main.BaseModel']]: def get_flat_models_from_fields(fields) -> Set[Type['main.BaseModel']]: """ Take a list of Pydantic ``Field``s (from a model) that could have been declared as sublcasses of ``BaseModel`` - (so, any of them could be a submodel), and generate a set with their models and all the submodels in the tree. + (so, any of them could be a submodel), and generate a set with their models and all the sub-models in the tree. I.e. if you pass a the fields of a model ``Foo`` (subclass of ``BaseModel``) as ``fields``, and on of them has a field of type ``Bar`` (also subclass of ``BaseModel``) and that model ``Bar`` has a field of type ``Baz`` (also subclass of ``BaseModel``), the return value will be ``set([Foo, Bar, Baz])``. :param fields: a list of Pydantic ``Field``s - :return: a set with any model declared in the fields, and all their submodels + :return: a set with any model declared in the fields, and all their sub-models """ flat_models = set() for field in fields: @@ -227,12 +224,9 @@ def get_flat_models_from_fields(fields) -> Set[Type['main.BaseModel']]: def get_flat_models_from_models(models: Sequence[Type['main.BaseModel']]) -> Set[Type['main.BaseModel']]: """ - Take a list of ``models`` and generate a set with them and all their submodels in their trees. I.e. if you pass + Take a list of ``models`` and generate a set with them and all their sub-models in their trees. I.e. if you pass a list of two models, ``Foo`` and ``Bar``, both subclasses of Pydantic ``BaseModel`` as models, and ``Bar`` has a field of type ``Baz`` (also subclass of ``BaseModel``), the return value will be ``set([Foo, Bar, Baz])``. - - :param model: a Pydantic ``BaseModel`` subclass - :return: a set with the initial model and all its submodels """ flat_models = set() for model in models: @@ -256,10 +250,7 @@ def field_type_schema( Used by ``field_schema()``, you probably should be using that function. Take a single ``field`` and generate the schema for its type only, not including additional - information as title, etc. Also return additional schema definitions, from submodels. - - :param model: a Pydantic ``Field`` subclass - :return: tuple of the type schema for this field and additional definitions form submodules + information as title, etc. Also return additional schema definitions, from sub-models. """ definitions = {} ref_prefix = ref_prefix or default_prefix @@ -316,12 +307,9 @@ def model_process_schema( """ Used by ``model_schema()``, you probably should be using that function. - Take a single ``model`` and generate its schema. Also return additional schema definitions, from submodels. The - submodels of the returned schema will be referenced, but their definitions will not be included in the schema. All + Take a single ``model`` and generate its schema. Also return additional schema definitions, from sub-models. The + sub-models of the returned schema will be referenced, but their definitions will not be included in the schema. All the definitions are returned as the second value. - - :param model: a Pydantic ``BaseModel`` subclass - :return: tuple of the schema for this model and additional definitions form submodules """ ref_prefix = ref_prefix or default_prefix s = {'title': model.__config__.title or model.__name__} @@ -341,10 +329,7 @@ def model_type_schema( You probably should be using ``model_schema()``, this function is indirectly used by that function. Take a single ``model`` and generate the schema for its type only, not including additional - information as title, etc. Also return additional schema definitions, from submodels. - - :param model: a Pydantic ``BaseModel`` subclass - :return: tuple of the type schema for this model and additional definitions form submodules + information as title, etc. Also return additional schema definitions, from sub-models. """ ref_prefix = ref_prefix or default_prefix properties = {} @@ -382,10 +367,6 @@ def field_singleton_sub_fields_schema( Take a list of Pydantic ``Field`` from the declaration of a type with parameters, and generate their schema. I.e., fields used as "type parameters", like ``str`` and ``int`` in ``Tuple[str, int]``. - - :param sub_fields: a list of Pydantic ``Field``s from a declaration using a type with parameters - :return: a tuple with the schema for the type declared with parameters and additional definitions from - any submodules """ ref_prefix = ref_prefix or default_prefix definitions = {} @@ -466,10 +447,7 @@ def field_singleton_schema( # noqa: C901 (ignore complexity) """ This function is indirectly used by ``field_schema()``, you probably should be using that function. - Take a single Pydantic ``Field``, and return its schema and any additional definitions from submodels. - - :param field: a single Pydantic ``Field`` - :return: a tuple with the schema for the type declared and additional definitions from any submodules + Take a single Pydantic ``Field``, and return its schema and any additional definitions from sub-models. """ ref_prefix = ref_prefix or default_prefix From 1aef32808cfd78a5a53b1dd2c6e01a012f6cfe6b Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Thu, 22 Nov 2018 14:49:34 +0000 Subject: [PATCH 63/65] support complex defaults --- pydantic/schema.py | 19 ++++++++++++++----- tests/test_schema.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/pydantic/schema.py b/pydantic/schema.py index 07cb3e860f..948b74ff2b 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -120,10 +120,7 @@ def field_schema( if not field.required and field.default is not None: schema_overrides = True - if isinstance(field.default, (int, float, str)): - s['default'] = field.default - else: - s['default'] = pydantic_encoder(field.default) + s['default'] = encode_default(field.default) if field._schema.extra: s.update(field._schema.extra) schema_overrides = True @@ -445,7 +442,7 @@ def field_singleton_schema( # noqa: C901 (ignore complexity) ref_prefix=None, ) -> Tuple[Dict[str, Any], Dict[str, Any]]: """ - This function is indirectly used by ``field_schema()``, you probably should be using that function. + This function is indirectly used by ``field_schema()``, you should probably be using that function. Take a single Pydantic ``Field``, and return its schema and any additional definitions from sub-models. """ @@ -494,3 +491,15 @@ def field_singleton_schema( # noqa: C901 (ignore complexity) else: return sub_schema, definitions raise ValueError(f'Value not declarable with JSON Schema, field: {field}') + + +def encode_default(dft): + if isinstance(dft, (int, float, str)): + return dft + elif isinstance(dft, (tuple, list, set)): + t = type(dft) + return t(encode_default(v) for v in dft) + elif isinstance(dft, dict): + return {encode_default(k): encode_default(v) for k, v in dft.items()} + else: + return pydantic_encoder(dft) diff --git a/tests/test_schema.py b/tests/test_schema.py index 9308e958ac..131b459089 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -826,3 +826,32 @@ class Baz(BaseModel): def test_schema_no_definitions(): model_schema = schema([], title='Schema without definitions') assert model_schema == {'title': 'Schema without definitions'} + + +def test_list_default(): + class UserModel(BaseModel): + friends: List[int] = [1] + + assert UserModel.schema() == { + 'title': 'UserModel', + 'type': 'object', + 'properties': {'friends': {'title': 'Friends', 'default': [1], 'type': 'array', 'items': {'type': 'integer'}}}, + } + + +def test_dict_default(): + class UserModel(BaseModel): + friends: Dict[int, float] = {1: 1.1, 2: 2.2} + + assert UserModel.schema() == { + 'title': 'UserModel', + 'type': 'object', + 'properties': { + 'friends': { + 'title': 'Friends', + 'default': {1: 1.1, 2: 2.2}, + 'type': 'object', + 'additionalProperties': {'type': 'number'}, + } + }, + } From 3ce82a6ce513c4d3ae6ecf87b234481de1615593 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Thu, 22 Nov 2018 15:49:46 +0000 Subject: [PATCH 64/65] use str not int as dict keys --- tests/test_schema.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_schema.py b/tests/test_schema.py index 131b459089..0ae4e8e404 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -841,7 +841,7 @@ class UserModel(BaseModel): def test_dict_default(): class UserModel(BaseModel): - friends: Dict[int, float] = {1: 1.1, 2: 2.2} + friends: Dict[str, float] = {'a': 1.1, 'b': 2.2} assert UserModel.schema() == { 'title': 'UserModel', @@ -849,7 +849,7 @@ class UserModel(BaseModel): 'properties': { 'friends': { 'title': 'Friends', - 'default': {1: 1.1, 2: 2.2}, + 'default': {'a': 1.1, 'b': 2.2}, 'type': 'object', 'additionalProperties': {'type': 'number'}, } From 229dfaa57755b65814dd8e9660a323ee795591ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 22 Nov 2018 19:54:11 +0400 Subject: [PATCH 65/65] Fix links to JSON Schema and OpenAPI --- docs/index.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 85af3a6670..095cbecb5b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -230,8 +230,9 @@ Outputs: .. literalinclude:: examples/schema1.json The generated schemas are compliant with the specifications: -`JSON Schema Core `__, -`JSON Schema Validation `__ and `OpenAPI `__. +`JSON Schema Core `__, +`JSON Schema Validation `__ and +`OpenAPI `__. ``BaseModel.schema`` will return a dict of the schema, while ``BaseModel.schema_json`` will return a JSON string representation of that.