diff --git a/changes/1102-samuelcolvin.md b/changes/1102-samuelcolvin.md
new file mode 100644
index 0000000..9cfe6ec
--- /dev/null
+++ b/changes/1102-samuelcolvin.md
@@ -0,0 +1 @@
+Fix for `__modify_schema__` when it conflicted with `field_class_to_schema*`
diff --git a/docs/build/schema_mapping.py b/docs/build/schema_mapping.py
index 16e97ac..0972c4b 100755
--- a/docs/build/schema_mapping.py
+++ b/docs/build/schema_mapping.py
@@ -6,6 +6,7 @@ Done like this rather than as a raw rst table to make future edits easier.
Please edit this file directly not .tmp_schema_mappings.html
"""
+import json
import re
from pathlib import Path
@@ -48,35 +49,35 @@ table = [
[
'list',
'array',
- '{"items": {}}',
+ {'items': {}},
'JSON Schema Core',
''
],
[
'tuple',
'array',
- '{"items": {}}',
+ {'items': {}},
'JSON Schema Core',
''
],
[
'set',
'array',
- '{"items": {}, {"uniqueItems": true}',
+ {'items': {}, 'uniqueItems': True},
'JSON Schema Validation',
''
],
[
'List[str]',
'array',
- '{"items": {"type": "string"}}',
+ {'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"}]}',
+ {'items': [{'type': 'string'}, {'type': 'integer'}]},
'JSON Schema Validation',
(
'And equivalently for any other set of subtypes. Note: If using schemas for OpenAPI, '
@@ -87,7 +88,7 @@ table = [
[
'Dict[str, int]',
'object',
- '{"additionalProperties": {"type": "integer"}}',
+ {'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 '
@@ -98,7 +99,7 @@ table = [
[
'Union[str, int]',
'anyOf',
- '{"anyOf": [{"type": "string"}, {"type": "integer"}]}',
+ {'anyOf': [{'type': 'string'}, {'type': 'integer'}]},
'JSON Schema Validation',
'And equivalently for any other subfields for unions.'
],
@@ -112,49 +113,42 @@ table = [
[
'SecretStr',
'string',
- '{"writeOnly": true}',
+ {'writeOnly': True},
'JSON Schema Validation',
''
],
[
'SecretBytes',
'string',
- '{"writeOnly": true}',
+ {'writeOnly': True},
'JSON Schema Validation',
''
],
[
'EmailStr',
'string',
- '{"format": "email"}',
+ {'format': 'email'},
'JSON Schema Validation',
''
],
[
'NameEmail',
'string',
- '{"format": "name-email"}',
+ {'format': 'name-email'},
'Pydantic standard "format" extension',
''
],
[
'AnyUrl',
'string',
- '{"format": "uri"}',
+ {'format': 'uri'},
'JSON Schema Validation',
''
],
- [
- 'DSN',
- 'string',
- '{"format": "dsn"}',
- 'Pydantic standard "format" extension',
- ''
- ],
[
'bytes',
'string',
- '{"format": "binary"}',
+ {'format': 'binary'},
'OpenAPI',
''
],
@@ -168,154 +162,154 @@ table = [
[
'UUID1',
'string',
- '{"format": "uuid1"}',
+ {'format': 'uuid1'},
'Pydantic standard "format" extension',
''
],
[
'UUID3',
'string',
- '{"format": "uuid3"}',
+ {'format': 'uuid3'},
'Pydantic standard "format" extension',
''
],
[
'UUID4',
'string',
- '{"format": "uuid4"}',
+ {'format': 'uuid4'},
'Pydantic standard "format" extension',
''
],
[
'UUID5',
'string',
- '{"format": "uuid5"}',
+ {'format': 'uuid5'},
'Pydantic standard "format" extension',
''
],
[
'UUID',
'string',
- '{"format": "uuid"}',
+ {'format': 'uuid'},
'Pydantic standard "format" extension',
'Suggested in OpenAPI.'
],
[
'FilePath',
'string',
- '{"format": "file-path"}',
+ {'format': 'file-path'},
'Pydantic standard "format" extension',
''
],
[
'DirectoryPath',
'string',
- '{"format": "directory-path"}',
+ {'format': 'directory-path'},
'Pydantic standard "format" extension',
''
],
[
'Path',
'string',
- '{"format": "path"}',
+ {'format': 'path'},
'Pydantic standard "format" extension',
''
],
[
'datetime',
'string',
- '{"format": "date-time"}',
+ {'format': 'date-time'},
'JSON Schema Validation',
''
],
[
'date',
'string',
- '{"format": "date"}',
+ {'format': 'date'},
'JSON Schema Validation',
''
],
[
'time',
'string',
- '{"format": "time"}',
+ {'format': 'time'},
'JSON Schema Validation',
''
],
[
'timedelta',
'number',
- '{"format": "time-delta"}',
+ {'format': 'time-delta'},
'Difference in seconds (a `float`), with Pydantic standard "format" extension',
'Suggested in JSON Schema repository\'s issues by maintainer.'
],
[
'Json',
'string',
- '{"format": "json-string"}',
+ {'format': 'json-string'},
'Pydantic standard "format" extension',
''
],
[
'IPv4Address',
'string',
- '{"format": "ipv4"}',
+ {'format': 'ipv4'},
'JSON Schema Validation',
''
],
[
'IPv6Address',
'string',
- '{"format": "ipv6"}',
+ {'format': 'ipv6'},
'JSON Schema Validation',
''
],
[
'IPvAnyAddress',
'string',
- '{"format": "ipvanyaddress"}',
+ {'format': 'ipvanyaddress'},
'Pydantic standard "format" extension',
'IPv4 or IPv6 address as used in `ipaddress` module',
],
[
'IPv4Interface',
'string',
- '{"format": "ipv4interface"}',
+ {'format': 'ipv4interface'},
'Pydantic standard "format" extension',
'IPv4 interface as used in `ipaddress` module',
],
[
'IPv6Interface',
'string',
- '{"format": "ipv6interface"}',
+ {'format': 'ipv6interface'},
'Pydantic standard "format" extension',
'IPv6 interface as used in `ipaddress` module',
],
[
'IPvAnyInterface',
'string',
- '{"format": "ipvanyinterface"}',
+ {'format': 'ipvanyinterface'},
'Pydantic standard "format" extension',
'IPv4 or IPv6 interface as used in `ipaddress` module',
],
[
'IPv4Network',
'string',
- '{"format": "ipv4network"}',
+ {'format': 'ipv4network'},
'Pydantic standard "format" extension',
'IPv4 network as used in `ipaddress` module',
],
[
'IPv6Network',
'string',
- '{"format": "ipv6network"}',
+ {'format': 'ipv6network'},
'Pydantic standard "format" extension',
'IPv6 network as used in `ipaddress` module',
],
[
'IPvAnyNetwork',
'string',
- '{"format": "ipvanynetwork"}',
+ {'format': 'ipvanynetwork'},
'Pydantic standard "format" extension',
'IPv4 or IPv6 network as used in `ipaddress` module',
],
@@ -346,7 +340,7 @@ table = [
[
'constr(regex=\'^text$\', min_length=2, max_length=10)',
'string',
- '{"pattern": "^text$", "minLength": 2, "maxLength": 10}',
+ {'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.'
],
@@ -363,21 +357,21 @@ table = [
[
'conint(gt=1, ge=2, lt=6, le=5, multiple_of=2)',
'integer',
- '{"maximum": 5, "exclusiveMaximum": 6, "minimum": 2, "exclusiveMinimum": 1, "multipleOf": 2}',
+ {'maximum': 5, 'exclusiveMaximum': 6, 'minimum': 2, 'exclusiveMinimum': 1, 'multipleOf': 2},
'',
'Any argument not passed to the function (not defined) will not be included in the schema.'
],
[
'PositiveInt',
'integer',
- '{"exclusiveMinimum": 0}',
+ {'exclusiveMinimum': 0},
'JSON Schema Validation',
''
],
[
'NegativeInt',
'integer',
- '{"exclusiveMaximum": 0}',
+ {'exclusiveMaximum': 0},
'JSON Schema Validation',
''
],
@@ -394,21 +388,21 @@ table = [
[
'confloat(gt=1, ge=2, lt=6, le=5, multiple_of=2)',
'number',
- '{"maximum": 5, "exclusiveMaximum": 6, "minimum": 2, "exclusiveMinimum": 1, "multipleOf": 2}',
+ {'maximum': 5, 'exclusiveMaximum': 6, 'minimum': 2, 'exclusiveMinimum': 1, 'multipleOf': 2},
'JSON Schema Validation',
'Any argument not passed to the function (not defined) will not be included in the schema.'
],
[
'PositiveFloat',
'number',
- '{"exclusiveMinimum": 0}',
+ {'exclusiveMinimum': 0},
'JSON Schema Validation',
''
],
[
'NegativeFloat',
'number',
- '{"exclusiveMaximum": 0}',
+ {'exclusiveMaximum': 0},
'JSON Schema Validation',
''
],
@@ -425,7 +419,7 @@ table = [
[
'condecimal(gt=1, ge=2, lt=6, le=5, multiple_of=2)',
'number',
- '{"maximum": 5, "exclusiveMaximum": 6, "minimum": 2, "exclusiveMinimum": 1, "multipleOf": 2}',
+ {'maximum': 5, 'exclusiveMaximum': 6, 'minimum': 2, 'exclusiveMinimum': 1, 'multipleOf': 2},
'JSON Schema Validation',
'Any argument not passed to the function (not defined) will not be included in the schema.'
],
@@ -439,7 +433,7 @@ table = [
[
'Color',
'string',
- '{"format": "color"}',
+ {'format': 'color'},
'Pydantic standard "format" extension',
'',
],
@@ -461,7 +455,8 @@ def build_schema_mappings():
rows = []
for py_type, json_type, additional, defined_in, notes in table:
-
+ if additional and not isinstance(additional, str):
+ additional = json.dumps(additional)
cols = [
f'{py_type}',
f'{json_type}',
@@ -479,7 +474,7 @@ def build_schema_mappings():
heading = '\n'.join(f'
{h} | ' for h in headings)
body = '\n\n\n'.join(rows)
text = f"""\
-
diff --git a/pydantic/color.py b/pydantic/color.py
index 75348c3..7b3970b 100644
--- a/pydantic/color.py
+++ b/pydantic/color.py
@@ -10,7 +10,7 @@ eg. Color((0, 255, 255)).as_named() == 'cyan' because "cyan" comes after "aqua".
import math
import re
from colorsys import hls_to_rgb, rgb_to_hls
-from typing import TYPE_CHECKING, Any, Optional, Tuple, Union, cast
+from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union, cast
from .errors import ColorError
from .utils import Representation, almost_equal_floats
@@ -75,6 +75,10 @@ class Color(Representation):
# if we've got here value must be a valid color
self._original = value
+ @classmethod
+ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
+ field_schema.update(type='string', format='color')
+
def original(self) -> ColorType:
"""
Original value passed to Color
diff --git a/pydantic/fields.py b/pydantic/fields.py
index 0af6ff7..b5a56f8 100644
--- a/pydantic/fields.py
+++ b/pydantic/fields.py
@@ -186,6 +186,7 @@ SHAPE_NAME_LOOKUP = {
class ModelField(Representation):
__slots__ = (
'type_',
+ 'outer_type_',
'sub_fields',
'key_field',
'validators',
@@ -222,6 +223,7 @@ class ModelField(Representation):
self.has_alias: bool = bool(alias)
self.alias: str = alias or name
self.type_: Any = type_
+ self.outer_type_: Any = type_
self.class_validators = class_validators or {}
self.default: Any = default
self.required: 'BoolUndefined' = required
@@ -298,6 +300,7 @@ class ModelField(Representation):
"""
if self.default is not None and self.type_ is None:
self.type_ = type(self.default)
+ self.outer_type_ = self.type_
if self.type_ is None:
raise errors_.ConfigError(f'unable to infer type for attribute "{self.name}"')
@@ -366,7 +369,10 @@ class ModelField(Representation):
types_.append(type_)
if len(types_) == 1:
+ # Optional[]
self.type_ = types_[0]
+ # this is the one case where the "outer type" isn't just the original type
+ self.outer_type_ = self.type_
# re-run to correctly interpret the new self.type_
self._type_analysis()
else:
@@ -647,6 +653,7 @@ class ModelField(Representation):
def _type_display(self) -> PyObjectStr:
t = display_as_type(self.type_)
+ # have to do this since display_as_type(self.outer_type_) is different (and wrong) on python 3.6
if self.shape == SHAPE_MAPPING:
t = f'Mapping[{display_as_type(self.key_field.type_)}, {t}]' # type: ignore
elif self.shape == SHAPE_TUPLE:
diff --git a/pydantic/networks.py b/pydantic/networks.py
index c34d903..593e3cd 100644
--- a/pydantic/networks.py
+++ b/pydantic/networks.py
@@ -141,7 +141,7 @@ class AnyUrl(str):
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
- update_not_none(field_schema, minLength=cls.min_length, maxLength=cls.max_length)
+ update_not_none(field_schema, minLength=cls.min_length, maxLength=cls.max_length, format='uri')
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
@@ -264,6 +264,10 @@ def stricturl(
class EmailStr(str):
+ @classmethod
+ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
+ field_schema.update(type='string', format='email')
+
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
# included here and below so the error happens straight away
@@ -285,6 +289,10 @@ class NameEmail(Representation):
self.name = name
self.email = email
+ @classmethod
+ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
+ field_schema.update(type='string', format='name-email')
+
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
if email_validator is None:
@@ -302,6 +310,10 @@ class NameEmail(Representation):
class IPvAnyAddress(_BaseAddress):
+ @classmethod
+ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
+ field_schema.update(type='string', format='ipvanyaddress')
+
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
yield cls.validate
@@ -320,6 +332,10 @@ class IPvAnyAddress(_BaseAddress):
class IPvAnyInterface(_BaseAddress):
+ @classmethod
+ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
+ field_schema.update(type='string', format='ipvanyinterface')
+
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
yield cls.validate
@@ -338,6 +354,10 @@ class IPvAnyInterface(_BaseAddress):
class IPvAnyNetwork(_BaseNetwork): # type: ignore
+ @classmethod
+ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
+ field_schema.update(type='string', format='ipvanynetwork')
+
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
yield cls.validate
diff --git a/pydantic/schema.py b/pydantic/schema.py
index 2c3b1e7..528525a 100644
--- a/pydantic/schema.py
+++ b/pydantic/schema.py
@@ -25,7 +25,6 @@ from typing import (
from uuid import UUID
from .class_validators import ROOT_KEY
-from .color import Color
from .fields import (
SHAPE_FROZENSET,
SHAPE_LIST,
@@ -39,23 +38,13 @@ from .fields import (
ModelField,
)
from .json import pydantic_encoder
-from .networks import AnyUrl, EmailStr, IPvAnyAddress, IPvAnyInterface, IPvAnyNetwork, NameEmail
+from .networks import AnyUrl, EmailStr
from .types import (
- UUID1,
- UUID3,
- UUID4,
- UUID5,
ConstrainedDecimal,
ConstrainedFloat,
ConstrainedInt,
ConstrainedList,
ConstrainedStr,
- DirectoryPath,
- FilePath,
- Json,
- SecretBytes,
- SecretStr,
- StrictBool,
conbytes,
condecimal,
confloat,
@@ -385,38 +374,34 @@ def field_type_schema(
"""
definitions = {}
nested_models: Set[str] = set()
+ f_schema: Dict[str, Any]
ref_prefix = ref_prefix or default_prefix
if field.shape in {SHAPE_LIST, SHAPE_TUPLE_ELLIPSIS, SHAPE_SEQUENCE, SHAPE_SET, SHAPE_FROZENSET}:
- f_schema, f_definitions, f_nested_models = field_singleton_schema(
+ items_schema, f_definitions, f_nested_models = field_singleton_schema(
field, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix, known_models=known_models
)
definitions.update(f_definitions)
nested_models.update(f_nested_models)
- s: Dict[str, Any] = {'type': 'array', 'items': f_schema}
+ f_schema = {'type': 'array', 'items': items_schema}
if field.shape in {SHAPE_SET, SHAPE_FROZENSET}:
- s['uniqueItems'] = True
- if field.field_info.min_items is not None:
- s['minItems'] = field.field_info.min_items
- if field.field_info.max_items is not None:
- s['maxItems'] = field.field_info.max_items
- return s, definitions, nested_models
+ f_schema['uniqueItems'] = True
+
elif field.shape == SHAPE_MAPPING:
- dict_schema: Dict[str, Any] = {'type': 'object'}
+ f_schema = {'type': 'object'}
key_field = cast(ModelField, field.key_field)
regex = getattr(key_field.type_, 'regex', None)
- f_schema, f_definitions, f_nested_models = field_singleton_schema(
+ items_schema, f_definitions, f_nested_models = field_singleton_schema(
field, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix, known_models=known_models
)
definitions.update(f_definitions)
nested_models.update(f_nested_models)
if regex:
# Dict keys have a regex pattern
- # f_schema might be a schema or empty dict, add it either way
- dict_schema['patternProperties'] = {regex.pattern: f_schema}
- elif f_schema:
+ # items_schema might be a schema or empty dict, add it either way
+ f_schema['patternProperties'] = {regex.pattern: items_schema}
+ elif items_schema:
# The dict values are not simply Any, so they need a schema
- dict_schema['additionalProperties'] = f_schema
- return dict_schema, definitions, nested_models
+ f_schema['additionalProperties'] = items_schema
elif field.shape == SHAPE_TUPLE:
sub_schema = []
sub_fields = cast(List[ModelField], field.sub_fields)
@@ -429,7 +414,7 @@ def field_type_schema(
sub_schema.append(sf_schema)
if len(sub_schema) == 1:
sub_schema = sub_schema[0] # type: ignore
- return {'type': 'array', 'items': sub_schema}, definitions, nested_models
+ f_schema = {'type': 'array', 'items': sub_schema}
else:
assert field.shape == SHAPE_SINGLETON, field.shape
f_schema, f_definitions, f_nested_models = field_singleton_schema(
@@ -442,7 +427,13 @@ def field_type_schema(
)
definitions.update(f_definitions)
nested_models.update(f_nested_models)
- return f_schema, definitions, nested_models
+
+ # check field type to avoid repeated calls to the same __modify_schema__ method
+ if field.type_ != field.outer_type_:
+ modify_schema = getattr(field.outer_type_, '__modify_schema__', None)
+ if modify_schema:
+ modify_schema(f_schema)
+ return f_schema, definitions, nested_models
def model_process_schema(
@@ -573,55 +564,35 @@ def field_singleton_sub_fields_schema(
return {'anyOf': sub_field_schemas}, definitions, nested_models
-# Order is important, subclasses of str must go before str, etc
-field_class_to_schema_enum_enabled: Tuple[Tuple[Any, Dict[str, Any]], ...] = (
- (EmailStr, {'type': 'string', 'format': 'email'}),
- (AnyUrl, {'type': 'string', 'format': 'uri'}),
- (SecretStr, {'type': 'string', 'writeOnly': True}),
- (str, {'type': 'string'}),
- (SecretBytes, {'type': 'string', 'writeOnly': True}),
- (bytes, {'type': 'string', 'format': 'binary'}),
- (StrictBool, {'type': 'boolean'}),
- (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'}),
- (dict, {'type': 'object'}),
- (list, {'type': 'array', 'items': {}}),
- (tuple, {'type': 'array', 'items': {}}),
- (set, {'type': 'array', 'items': {}, 'uniqueItems': True}),
- (Color, {'type': 'string', 'format': 'color'}),
-)
-
-json_scheme = {'type': 'string', 'format': 'json-string'}
-
-# 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'}),
+# Order is important, e.g. subclasses of str must go before str
+# this is used only for standard library types, custom types should use __modify_schema__ instead
+field_class_to_schema: Tuple[Tuple[Any, Dict[str, Any]], ...] = (
(Path, {'type': 'string', 'format': 'path'}),
(datetime, {'type': 'string', 'format': 'date-time'}),
(date, {'type': 'string', 'format': 'date'}),
(time, {'type': 'string', 'format': 'time'}),
(timedelta, {'type': 'number', 'format': 'time-delta'}),
- (Json, json_scheme),
(IPv4Network, {'type': 'string', 'format': 'ipv4network'}),
(IPv6Network, {'type': 'string', 'format': 'ipv6network'}),
- (IPvAnyNetwork, {'type': 'string', 'format': 'ipvanynetwork'}),
(IPv4Interface, {'type': 'string', 'format': 'ipv4interface'}),
(IPv6Interface, {'type': 'string', 'format': 'ipv6interface'}),
- (IPvAnyInterface, {'type': 'string', 'format': 'ipvanyinterface'}),
(IPv4Address, {'type': 'string', 'format': 'ipv4'}),
(IPv6Address, {'type': 'string', 'format': 'ipv6'}),
- (IPvAnyAddress, {'type': 'string', 'format': 'ipvanyaddress'}),
+ (str, {'type': 'string'}),
+ (bytes, {'type': 'string', 'format': 'binary'}),
+ (bool, {'type': 'boolean'}),
+ (int, {'type': 'integer'}),
+ (float, {'type': 'number'}),
+ (Decimal, {'type': 'number'}),
+ (UUID, {'type': 'string', 'format': 'uuid'}),
+ (dict, {'type': 'object'}),
+ (list, {'type': 'array', 'items': {}}),
+ (tuple, {'type': 'array', 'items': {}}),
+ (set, {'type': 'array', 'items': {}, 'uniqueItems': True}),
)
+json_scheme = {'type': 'string', 'format': 'json-string'}
+
def field_singleton_schema( # noqa: C901 (ignore complexity)
field: ModelField,
@@ -652,10 +623,7 @@ def field_singleton_schema( # noqa: C901 (ignore complexity)
known_models=known_models,
)
if field.type_ is Any or type(field.type_) == TypeVar:
- if field.parse_json:
- return json_scheme, definitions, nested_models
- else:
- return {}, definitions, nested_models # no restrictions
+ return {}, definitions, nested_models # no restrictions
if is_callable_type(field.type_):
raise SkipField(f'Callable {field.name} was excluded from schema since JSON schema has no equivalent type.')
f_schema: Dict[str, Any] = {}
@@ -677,11 +645,12 @@ def field_singleton_schema( # noqa: C901 (ignore complexity)
literal_value = values[0]
field_type = type(literal_value)
f_schema['const'] = literal_value
+
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 type_, t_schema in field_class_to_schema_enum_enabled:
+ for type_, t_schema in field_class_to_schema:
if issubclass(field_type, type_):
f_schema.update(t_schema)
break
@@ -690,16 +659,13 @@ def field_singleton_schema( # noqa: C901 (ignore complexity)
if modify_schema:
modify_schema(f_schema)
- # Return schema, with or without enum definitions
if f_schema:
return f_schema, definitions, nested_models
- for type_, t_schema in field_class_to_schema_enum_disabled:
- if issubclass(field_type, type_):
- return t_schema, definitions, nested_models
# Handle dataclass-based models
if lenient_issubclass(getattr(field_type, '__pydantic_model__', None), BaseModel):
field_type = field_type.__pydantic_model__
+
if issubclass(field_type, BaseModel):
model_name = model_name_map[field_type]
if field_type not in known_models:
@@ -720,6 +686,7 @@ def field_singleton_schema( # noqa: C901 (ignore complexity)
return schema_ref, definitions, nested_models
else:
return {'allOf': [schema_ref]}, definitions, nested_models
+
raise ValueError(f'Value not declarable with JSON Schema, field: {field}')
diff --git a/pydantic/types.py b/pydantic/types.py
index 90e00fc..a19319f 100644
--- a/pydantic/types.py
+++ b/pydantic/types.py
@@ -92,9 +92,7 @@ class ConstrainedBytes(bytes):
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
- update_not_none(
- field_schema, minLength=cls.min_length, maxLength=cls.max_length,
- )
+ update_not_none(field_schema, minLength=cls.min_length, maxLength=cls.max_length)
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
@@ -129,9 +127,7 @@ class ConstrainedList(list): # type: ignore
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
- update_not_none(
- field_schema, minLength=cls.min_items, maxLength=cls.max_items,
- )
+ update_not_none(field_schema, minItems=cls.min_items, maxItems=cls.max_items)
@classmethod
def list_length_validator(cls, v: 'List[T]') -> 'List[T]':
@@ -220,6 +216,10 @@ else:
StrictBool to allow for bools which are not type-coerced.
"""
+ @classmethod
+ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
+ field_schema.update(type='boolean')
+
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
yield cls.validate
@@ -453,20 +453,28 @@ def condecimal(
class UUID1(UUID):
_required_version = 1
+ @classmethod
+ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
+ field_schema.update(type='string', format=f'uuid{cls._required_version}')
-class UUID3(UUID):
+
+class UUID3(UUID1):
_required_version = 3
-class UUID4(UUID):
+class UUID4(UUID1):
_required_version = 4
-class UUID5(UUID):
+class UUID5(UUID1):
_required_version = 5
class FilePath(Path):
+ @classmethod
+ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
+ field_schema.update(format='file-path')
+
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
yield path_validator
@@ -482,6 +490,10 @@ class FilePath(Path):
class DirectoryPath(Path):
+ @classmethod
+ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
+ field_schema.update(format='directory-path')
+
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
yield path_validator
@@ -506,10 +518,16 @@ class JsonMeta(type):
class Json(metaclass=JsonMeta):
- pass
+ @classmethod
+ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
+ field_schema.update(type='string', format='json-string')
class SecretStr:
+ @classmethod
+ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
+ field_schema.update(type='string', writeOnly=True)
+
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
yield str_validator
@@ -540,6 +558,10 @@ class SecretStr:
class SecretBytes:
+ @classmethod
+ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
+ field_schema.update(type='string', writeOnly=True)
+
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
yield bytes_validator
diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py
index 7ed3082..74cd295 100644
--- a/tests/test_edge_cases.py
+++ b/tests/test_edge_cases.py
@@ -2,7 +2,7 @@ import re
import sys
from decimal import Decimal
from enum import Enum
-from typing import Any, Dict, List, Optional, Set, Tuple, Type, TypeVar, Union
+from typing import Any, Dict, FrozenSet, List, Optional, Set, Tuple, Type, TypeVar, Union
import pytest
@@ -1222,8 +1222,10 @@ def test_field_str_shape():
(Union[List[int], Set[bytes]], 'Union[List[int], Set[bytes]]'),
(List[Tuple[int, int]], 'List[Tuple[int, int]]'),
(Dict[int, str], 'Mapping[int, str]'),
+ (FrozenSet[int], 'FrozenSet[int]'),
(Tuple[int, ...], 'Tuple[int, ...]'),
(Optional[List[int]], 'Optional[List[int]]'),
+ (dict, 'dict'),
],
)
def test_field_type_display(type_, expected):
diff --git a/tests/test_schema.py b/tests/test_schema.py
index c1fd59f..3a96360 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -516,8 +516,7 @@ def test_str_constrained_types(field_type, expected_schema):
model_schema = Model.schema()
assert model_schema['properties']['a'] == expected_schema
- base_schema = {'title': 'Model', 'type': 'object', 'properties': {'a': {}}, 'required': ['a']}
- base_schema['properties']['a'] = expected_schema
+ base_schema = {'title': 'Model', 'type': 'object', 'properties': {'a': expected_schema}, 'required': ['a']}
assert model_schema == base_schema
@@ -670,12 +669,16 @@ def test_path_types(field_type, expected_schema):
def test_json_type():
class Model(BaseModel):
a: Json
+ b: Json[int]
- model_schema = Model.schema()
- assert model_schema == {
+ assert Model.schema() == {
'title': 'Model',
'type': 'object',
- 'properties': {'a': {'title': 'A', 'type': 'string', 'format': 'json-string'}},
+ 'properties': {
+ 'a': {'title': 'A', 'type': 'string', 'format': 'json-string'},
+ 'b': {'title': 'B', 'type': 'integer'},
+ },
+ 'required': ['b'],
}
@@ -1496,10 +1499,7 @@ def test_model_with_schema_extra_callable():
schema.pop('properties')
schema['type'] = 'override'
- assert Model.schema() == {
- 'title': 'Model',
- 'type': 'override',
- }
+ assert Model.schema() == {'title': 'Model', 'type': 'override'}
def test_model_with_extra_forbidden():
@@ -1613,8 +1613,9 @@ def test_real_vs_phony_constraints():
def test_conlist():
class Model(BaseModel):
foo: List[int] = Field(..., min_items=2, max_items=4)
+ bar: conlist(str, min_items=1, max_items=4) = None
- assert Model(foo=[1, 2]).dict() == {'foo': [1, 2]}
+ assert Model(foo=[1, 2], bar=['spoon']).dict() == {'foo': [1, 2], 'bar': ['spoon']}
with pytest.raises(ValidationError, match='ensure this value has at least 2 items'):
Model(foo=[1])
@@ -1626,7 +1627,8 @@ def test_conlist():
'title': 'Model',
'type': 'object',
'properties': {
- 'foo': {'title': 'Foo', 'type': 'array', 'items': {'type': 'integer'}, 'minItems': 2, 'maxItems': 4}
+ 'foo': {'title': 'Foo', 'type': 'array', 'items': {'type': 'integer'}, 'minItems': 2, 'maxItems': 4},
+ 'bar': {'title': 'Bar', 'type': 'array', 'items': {'type': 'string'}, 'minItems': 1, 'maxItems': 4},
},
'required': ['foo'],
}
@@ -1706,6 +1708,27 @@ def test_schema_attributes():
}
+def test_path_modify_schema():
+ class MyPath(Path):
+ @classmethod
+ def __modify_schema__(cls, schema):
+ schema.update(foobar=123)
+
+ class Model(BaseModel):
+ path1: Path
+ path2: MyPath
+
+ assert Model.schema() == {
+ 'title': 'Model',
+ 'type': 'object',
+ 'properties': {
+ 'path1': {'title': 'Path1', 'type': 'string', 'format': 'path'},
+ 'path2': {'title': 'Path2', 'type': 'string', 'format': 'path', 'foobar': 123},
+ },
+ 'required': ['path1', 'path2'],
+ }
+
+
def test_frozen_set():
class Model(BaseModel):
a: FrozenSet[int] = frozenset({1, 2, 3})