1import math
2import os
3import sys
4import tempfile
5from datetime import date, datetime, time, timedelta
6from decimal import Decimal
7from enum import Enum, IntEnum
8from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network
9from pathlib import Path
10from typing import (
11    Any,
12    Callable,
13    Dict,
14    FrozenSet,
15    Generic,
16    Iterable,
17    List,
18    NewType,
19    Optional,
20    Set,
21    Tuple,
22    TypeVar,
23    Union,
24)
25from uuid import UUID
26
27import pytest
28from typing_extensions import Literal
29
30from pydantic import BaseModel, Extra, Field, ValidationError, conlist, conset, validator
31from pydantic.color import Color
32from pydantic.dataclasses import dataclass
33from pydantic.networks import AnyUrl, EmailStr, IPvAnyAddress, IPvAnyInterface, IPvAnyNetwork, NameEmail, stricturl
34from pydantic.schema import (
35    get_flat_models_from_model,
36    get_flat_models_from_models,
37    get_model_name_map,
38    model_process_schema,
39    model_schema,
40    schema,
41)
42from pydantic.types import (
43    UUID1,
44    UUID3,
45    UUID4,
46    UUID5,
47    ConstrainedBytes,
48    ConstrainedDecimal,
49    ConstrainedFloat,
50    ConstrainedInt,
51    ConstrainedStr,
52    DirectoryPath,
53    FilePath,
54    Json,
55    NegativeFloat,
56    NegativeInt,
57    NoneBytes,
58    NoneStr,
59    NoneStrBytes,
60    NonNegativeFloat,
61    NonNegativeInt,
62    NonPositiveFloat,
63    NonPositiveInt,
64    PositiveFloat,
65    PositiveInt,
66    PyObject,
67    SecretBytes,
68    SecretStr,
69    StrBytes,
70    StrictBool,
71    StrictStr,
72    conbytes,
73    condecimal,
74    confloat,
75    conint,
76    constr,
77)
78
79try:
80    import email_validator
81except ImportError:
82    email_validator = None
83
84
85def test_key():
86    class ApplePie(BaseModel):
87        """
88        This is a test.
89        """
90
91        a: float
92        b: int = 10
93
94    s = {
95        'type': 'object',
96        'properties': {'a': {'type': 'number', 'title': 'A'}, 'b': {'type': 'integer', 'title': 'B', 'default': 10}},
97        'required': ['a'],
98        'title': 'ApplePie',
99        'description': 'This is a test.',
100    }
101    assert ApplePie.__schema_cache__.keys() == set()
102    assert ApplePie.schema() == s
103    assert ApplePie.__schema_cache__.keys() == {(True, '#/definitions/{model}')}
104    assert ApplePie.schema() == s
105
106
107def test_by_alias():
108    class ApplePie(BaseModel):
109        a: float
110        b: int = 10
111
112        class Config:
113            title = 'Apple Pie'
114            fields = {'a': 'Snap', 'b': 'Crackle'}
115
116    assert ApplePie.schema() == {
117        'type': 'object',
118        'title': 'Apple Pie',
119        'properties': {
120            'Snap': {'type': 'number', 'title': 'Snap'},
121            'Crackle': {'type': 'integer', 'title': 'Crackle', 'default': 10},
122        },
123        'required': ['Snap'],
124    }
125    assert list(ApplePie.schema(by_alias=True)['properties'].keys()) == ['Snap', 'Crackle']
126    assert list(ApplePie.schema(by_alias=False)['properties'].keys()) == ['a', 'b']
127
128
129def test_ref_template():
130    class KeyLimePie(BaseModel):
131        x: str = None
132
133    class ApplePie(BaseModel):
134        a: float = None
135        key_lime: KeyLimePie = None
136
137        class Config:
138            title = 'Apple Pie'
139
140    assert ApplePie.schema(ref_template='foobar/{model}.json') == {
141        'title': 'Apple Pie',
142        'type': 'object',
143        'properties': {'a': {'title': 'A', 'type': 'number'}, 'key_lime': {'$ref': 'foobar/KeyLimePie.json'}},
144        'definitions': {
145            'KeyLimePie': {
146                'title': 'KeyLimePie',
147                'type': 'object',
148                'properties': {'x': {'title': 'X', 'type': 'string'}},
149            },
150        },
151    }
152    assert ApplePie.schema()['properties']['key_lime'] == {'$ref': '#/definitions/KeyLimePie'}
153    json_schema = ApplePie.schema_json(ref_template='foobar/{model}.json')
154    assert 'foobar/KeyLimePie.json' in json_schema
155    assert '#/definitions/KeyLimePie' not in json_schema
156
157
158def test_by_alias_generator():
159    class ApplePie(BaseModel):
160        a: float
161        b: int = 10
162
163        class Config:
164            @staticmethod
165            def alias_generator(x):
166                return x.upper()
167
168    assert ApplePie.schema() == {
169        'title': 'ApplePie',
170        'type': 'object',
171        'properties': {'A': {'title': 'A', 'type': 'number'}, 'B': {'title': 'B', 'default': 10, 'type': 'integer'}},
172        'required': ['A'],
173    }
174    assert ApplePie.schema(by_alias=False)['properties'].keys() == {'a', 'b'}
175
176
177def test_sub_model():
178    class Foo(BaseModel):
179        """hello"""
180
181        b: float
182
183    class Bar(BaseModel):
184        a: int
185        b: Foo = None
186
187    assert Bar.schema() == {
188        'type': 'object',
189        'title': 'Bar',
190        'definitions': {
191            'Foo': {
192                'type': 'object',
193                'title': 'Foo',
194                'description': 'hello',
195                'properties': {'b': {'type': 'number', 'title': 'B'}},
196                'required': ['b'],
197            }
198        },
199        'properties': {'a': {'type': 'integer', 'title': 'A'}, 'b': {'$ref': '#/definitions/Foo'}},
200        'required': ['a'],
201    }
202
203
204def test_schema_class():
205    class Model(BaseModel):
206        foo: int = Field(4, title='Foo is Great')
207        bar: str = Field(..., description='this description of bar')
208
209    with pytest.raises(ValidationError):
210        Model()
211
212    m = Model(bar=123)
213    assert m.dict() == {'foo': 4, 'bar': '123'}
214
215    assert Model.schema() == {
216        'type': 'object',
217        'title': 'Model',
218        'properties': {
219            'foo': {'type': 'integer', 'title': 'Foo is Great', 'default': 4},
220            'bar': {'type': 'string', 'title': 'Bar', 'description': 'this description of bar'},
221        },
222        'required': ['bar'],
223    }
224
225
226def test_schema_repr():
227    s = Field(4, title='Foo is Great')
228    assert str(s) == "default=4 title='Foo is Great' extra={}"
229    assert repr(s) == "FieldInfo(default=4, title='Foo is Great', extra={})"
230
231
232def test_schema_class_by_alias():
233    class Model(BaseModel):
234        foo: int = Field(4, alias='foofoo')
235
236    assert list(Model.schema()['properties'].keys()) == ['foofoo']
237    assert list(Model.schema(by_alias=False)['properties'].keys()) == ['foo']
238
239
240def test_choices():
241    FooEnum = Enum('FooEnum', {'foo': 'f', 'bar': 'b'})
242    BarEnum = IntEnum('BarEnum', {'foo': 1, 'bar': 2})
243
244    class SpamEnum(str, Enum):
245        foo = 'f'
246        bar = 'b'
247
248    class Model(BaseModel):
249        foo: FooEnum
250        bar: BarEnum
251        spam: SpamEnum = Field(None)
252
253    assert Model.schema() == {
254        'title': 'Model',
255        'type': 'object',
256        'properties': {
257            'foo': {'$ref': '#/definitions/FooEnum'},
258            'bar': {'$ref': '#/definitions/BarEnum'},
259            'spam': {'$ref': '#/definitions/SpamEnum'},
260        },
261        'required': ['foo', 'bar'],
262        'definitions': {
263            'FooEnum': {'title': 'FooEnum', 'description': 'An enumeration.', 'enum': ['f', 'b']},
264            'BarEnum': {'title': 'BarEnum', 'description': 'An enumeration.', 'type': 'integer', 'enum': [1, 2]},
265            'SpamEnum': {'title': 'SpamEnum', 'description': 'An enumeration.', 'type': 'string', 'enum': ['f', 'b']},
266        },
267    }
268
269
270def test_enum_modify_schema():
271    class SpamEnum(str, Enum):
272        foo = 'f'
273        bar = 'b'
274
275        @classmethod
276        def __modify_schema__(cls, field_schema):
277            field_schema['tsEnumNames'] = [e.name for e in cls]
278
279    class Model(BaseModel):
280        spam: SpamEnum = Field(None)
281
282    assert Model.schema() == {
283        'definitions': {
284            'SpamEnum': {
285                'description': 'An enumeration.',
286                'enum': ['f', 'b'],
287                'title': 'SpamEnum',
288                'tsEnumNames': ['foo', 'bar'],
289                'type': 'string',
290            }
291        },
292        'properties': {'spam': {'$ref': '#/definitions/SpamEnum'}},
293        'title': 'Model',
294        'type': 'object',
295    }
296
297
298def test_enum_schema_custom_field():
299    class FooBarEnum(str, Enum):
300        foo = 'foo'
301        bar = 'bar'
302
303    class Model(BaseModel):
304        pika: FooBarEnum = Field(alias='pikalias', title='Pikapika!', description='Pika is definitely the best!')
305        bulbi: FooBarEnum = Field('foo', alias='bulbialias', title='Bulbibulbi!', description='Bulbi is not...')
306        cara: FooBarEnum
307
308    assert Model.schema() == {
309        'definitions': {
310            'FooBarEnum': {
311                'description': 'An enumeration.',
312                'enum': ['foo', 'bar'],
313                'title': 'FooBarEnum',
314                'type': 'string',
315            }
316        },
317        'properties': {
318            'pikalias': {
319                'allOf': [{'$ref': '#/definitions/FooBarEnum'}],
320                'description': 'Pika is definitely the best!',
321                'title': 'Pikapika!',
322            },
323            'bulbialias': {
324                'allOf': [{'$ref': '#/definitions/FooBarEnum'}],
325                'description': 'Bulbi is not...',
326                'title': 'Bulbibulbi!',
327                'default': 'foo',
328            },
329            'cara': {'$ref': '#/definitions/FooBarEnum'},
330        },
331        'required': ['pikalias', 'cara'],
332        'title': 'Model',
333        'type': 'object',
334    }
335
336
337def test_enum_and_model_have_same_behaviour():
338    class Names(str, Enum):
339        rick = 'Rick'
340        morty = 'Morty'
341        summer = 'Summer'
342
343    class Pika(BaseModel):
344        a: str
345
346    class Foo(BaseModel):
347        enum: Names
348        titled_enum: Names = Field(
349            ...,
350            title='Title of enum',
351            description='Description of enum',
352        )
353        model: Pika
354        titled_model: Pika = Field(
355            ...,
356            title='Title of model',
357            description='Description of model',
358        )
359
360    assert Foo.schema() == {
361        'definitions': {
362            'Pika': {
363                'properties': {'a': {'title': 'A', 'type': 'string'}},
364                'required': ['a'],
365                'title': 'Pika',
366                'type': 'object',
367            },
368            'Names': {
369                'description': 'An enumeration.',
370                'enum': ['Rick', 'Morty', 'Summer'],
371                'title': 'Names',
372                'type': 'string',
373            },
374        },
375        'properties': {
376            'enum': {'$ref': '#/definitions/Names'},
377            'model': {'$ref': '#/definitions/Pika'},
378            'titled_enum': {
379                'allOf': [{'$ref': '#/definitions/Names'}],
380                'description': 'Description of enum',
381                'title': 'Title of enum',
382            },
383            'titled_model': {
384                'allOf': [{'$ref': '#/definitions/Pika'}],
385                'description': 'Description of model',
386                'title': 'Title of model',
387            },
388        },
389        'required': ['enum', 'titled_enum', 'model', 'titled_model'],
390        'title': 'Foo',
391        'type': 'object',
392    }
393
394
395def test_list_enum_schema_extras():
396    class FoodChoice(str, Enum):
397        spam = 'spam'
398        egg = 'egg'
399        chips = 'chips'
400
401    class Model(BaseModel):
402        foods: List[FoodChoice] = Field(examples=[['spam', 'egg']])
403
404    assert Model.schema() == {
405        'definitions': {
406            'FoodChoice': {
407                'description': 'An enumeration.',
408                'enum': ['spam', 'egg', 'chips'],
409                'title': 'FoodChoice',
410                'type': 'string',
411            }
412        },
413        'properties': {
414            'foods': {'type': 'array', 'items': {'$ref': '#/definitions/FoodChoice'}, 'examples': [['spam', 'egg']]},
415        },
416        'required': ['foods'],
417        'title': 'Model',
418        'type': 'object',
419    }
420
421
422def test_json_schema():
423    class Model(BaseModel):
424        a = b'foobar'
425        b = Decimal('12.34')
426
427    assert Model.schema_json(indent=2) == (
428        '{\n'
429        '  "title": "Model",\n'
430        '  "type": "object",\n'
431        '  "properties": {\n'
432        '    "a": {\n'
433        '      "title": "A",\n'
434        '      "default": "foobar",\n'
435        '      "type": "string",\n'
436        '      "format": "binary"\n'
437        '    },\n'
438        '    "b": {\n'
439        '      "title": "B",\n'
440        '      "default": 12.34,\n'
441        '      "type": "number"\n'
442        '    }\n'
443        '  }\n'
444        '}'
445    )
446
447
448def test_list_sub_model():
449    class Foo(BaseModel):
450        a: float
451
452    class Bar(BaseModel):
453        b: List[Foo]
454
455    assert Bar.schema() == {
456        'title': 'Bar',
457        'type': 'object',
458        'definitions': {
459            'Foo': {
460                'title': 'Foo',
461                'type': 'object',
462                'properties': {'a': {'type': 'number', 'title': 'A'}},
463                'required': ['a'],
464            }
465        },
466        'properties': {'b': {'type': 'array', 'items': {'$ref': '#/definitions/Foo'}, 'title': 'B'}},
467        'required': ['b'],
468    }
469
470
471def test_optional():
472    class Model(BaseModel):
473        a: Optional[str]
474
475    assert Model.schema() == {'title': 'Model', 'type': 'object', 'properties': {'a': {'type': 'string', 'title': 'A'}}}
476
477
478def test_any():
479    class Model(BaseModel):
480        a: Any
481
482    assert Model.schema() == {'title': 'Model', 'type': 'object', 'properties': {'a': {'title': 'A'}}}
483
484
485def test_set():
486    class Model(BaseModel):
487        a: Set[int]
488        b: set
489
490    assert Model.schema() == {
491        'title': 'Model',
492        'type': 'object',
493        'properties': {
494            'a': {'title': 'A', 'type': 'array', 'uniqueItems': True, 'items': {'type': 'integer'}},
495            'b': {'title': 'B', 'type': 'array', 'items': {}, 'uniqueItems': True},
496        },
497        'required': ['a', 'b'],
498    }
499
500
501def test_const_str():
502    class Model(BaseModel):
503        a: str = Field('some string', const=True)
504
505    assert Model.schema() == {
506        'title': 'Model',
507        'type': 'object',
508        'properties': {'a': {'title': 'A', 'type': 'string', 'const': 'some string'}},
509    }
510
511
512def test_const_false():
513    class Model(BaseModel):
514        a: str = Field('some string', const=False)
515
516    assert Model.schema() == {
517        'title': 'Model',
518        'type': 'object',
519        'properties': {'a': {'title': 'A', 'type': 'string', 'default': 'some string'}},
520    }
521
522
523@pytest.mark.parametrize(
524    'field_type,expected_schema',
525    [
526        (tuple, {}),
527        (
528            Tuple[str, int, Union[str, int, float], float],
529            [
530                {'type': 'string'},
531                {'type': 'integer'},
532                {'anyOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'number'}]},
533                {'type': 'number'},
534            ],
535        ),
536        (Tuple[str], {'type': 'string'}),
537    ],
538)
539def test_tuple(field_type, expected_schema):
540    class Model(BaseModel):
541        a: field_type
542
543    base_schema = {
544        'title': 'Model',
545        'type': 'object',
546        'properties': {'a': {'title': 'A', 'type': 'array'}},
547        'required': ['a'],
548    }
549    base_schema['properties']['a']['items'] = expected_schema
550
551    assert Model.schema() == base_schema
552
553
554def test_bool():
555    class Model(BaseModel):
556        a: bool
557
558    assert Model.schema() == {
559        'title': 'Model',
560        'type': 'object',
561        'properties': {'a': {'title': 'A', 'type': 'boolean'}},
562        'required': ['a'],
563    }
564
565
566def test_strict_bool():
567    class Model(BaseModel):
568        a: StrictBool
569
570    assert Model.schema() == {
571        'title': 'Model',
572        'type': 'object',
573        'properties': {'a': {'title': 'A', 'type': 'boolean'}},
574        'required': ['a'],
575    }
576
577
578def test_dict():
579    class Model(BaseModel):
580        a: dict
581
582    assert Model.schema() == {
583        'title': 'Model',
584        'type': 'object',
585        'properties': {'a': {'title': 'A', 'type': 'object'}},
586        'required': ['a'],
587    }
588
589
590def test_list():
591    class Model(BaseModel):
592        a: list
593
594    assert Model.schema() == {
595        'title': 'Model',
596        'type': 'object',
597        'properties': {'a': {'title': 'A', 'type': 'array', 'items': {}}},
598        'required': ['a'],
599    }
600
601
602class Foo(BaseModel):
603    a: float
604
605
606@pytest.mark.parametrize(
607    'field_type,expected_schema',
608    [
609        (
610            Union[int, str],
611            {
612                'properties': {'a': {'title': 'A', 'anyOf': [{'type': 'integer'}, {'type': 'string'}]}},
613                'required': ['a'],
614            },
615        ),
616        (
617            List[int],
618            {'properties': {'a': {'title': 'A', 'type': 'array', 'items': {'type': 'integer'}}}, 'required': ['a']},
619        ),
620        (
621            Dict[str, Foo],
622            {
623                'definitions': {
624                    'Foo': {
625                        'title': 'Foo',
626                        'type': 'object',
627                        'properties': {'a': {'title': 'A', 'type': 'number'}},
628                        'required': ['a'],
629                    }
630                },
631                'properties': {
632                    'a': {'title': 'A', 'type': 'object', 'additionalProperties': {'$ref': '#/definitions/Foo'}}
633                },
634                'required': ['a'],
635            },
636        ),
637        (
638            Union[None, Foo],
639            {
640                'definitions': {
641                    'Foo': {
642                        'title': 'Foo',
643                        'type': 'object',
644                        'properties': {'a': {'title': 'A', 'type': 'number'}},
645                        'required': ['a'],
646                    }
647                },
648                'properties': {'a': {'$ref': '#/definitions/Foo'}},
649            },
650        ),
651        (Dict[str, Any], {'properties': {'a': {'title': 'A', 'type': 'object'}}, 'required': ['a']}),
652    ],
653)
654def test_list_union_dict(field_type, expected_schema):
655    class Model(BaseModel):
656        a: field_type
657
658    base_schema = {'title': 'Model', 'type': 'object'}
659    base_schema.update(expected_schema)
660
661    assert Model.schema() == base_schema
662
663
664@pytest.mark.parametrize(
665    'field_type,expected_schema',
666    [
667        (datetime, {'type': 'string', 'format': 'date-time'}),
668        (date, {'type': 'string', 'format': 'date'}),
669        (time, {'type': 'string', 'format': 'time'}),
670        (timedelta, {'type': 'number', 'format': 'time-delta'}),
671    ],
672)
673def test_date_types(field_type, expected_schema):
674    class Model(BaseModel):
675        a: field_type
676
677    attribute_schema = {'title': 'A'}
678    attribute_schema.update(expected_schema)
679
680    base_schema = {'title': 'Model', 'type': 'object', 'properties': {'a': attribute_schema}, 'required': ['a']}
681
682    assert Model.schema() == base_schema
683
684
685@pytest.mark.parametrize(
686    'field_type,expected_schema',
687    [
688        (NoneStr, {'properties': {'a': {'title': 'A', 'type': 'string'}}}),
689        (NoneBytes, {'properties': {'a': {'title': 'A', 'type': 'string', 'format': 'binary'}}}),
690        (
691            StrBytes,
692            {
693                'properties': {
694                    'a': {'title': 'A', 'anyOf': [{'type': 'string'}, {'type': 'string', 'format': 'binary'}]}
695                },
696                'required': ['a'],
697            },
698        ),
699        (
700            NoneStrBytes,
701            {
702                'properties': {
703                    'a': {'title': 'A', 'anyOf': [{'type': 'string'}, {'type': 'string', 'format': 'binary'}]}
704                }
705            },
706        ),
707    ],
708)
709def test_str_basic_types(field_type, expected_schema):
710    class Model(BaseModel):
711        a: field_type
712
713    base_schema = {'title': 'Model', 'type': 'object'}
714    base_schema.update(expected_schema)
715    assert Model.schema() == base_schema
716
717
718@pytest.mark.parametrize(
719    'field_type,expected_schema',
720    [
721        (StrictStr, {'title': 'A', 'type': 'string'}),
722        (ConstrainedStr, {'title': 'A', 'type': 'string'}),
723        (
724            constr(min_length=3, max_length=5, regex='^text$'),
725            {'title': 'A', 'type': 'string', 'minLength': 3, 'maxLength': 5, 'pattern': '^text$'},
726        ),
727    ],
728)
729def test_str_constrained_types(field_type, expected_schema):
730    class Model(BaseModel):
731        a: field_type
732
733    model_schema = Model.schema()
734    assert model_schema['properties']['a'] == expected_schema
735
736    base_schema = {'title': 'Model', 'type': 'object', 'properties': {'a': expected_schema}, 'required': ['a']}
737
738    assert model_schema == base_schema
739
740
741@pytest.mark.parametrize(
742    'field_type,expected_schema',
743    [
744        (AnyUrl, {'title': 'A', 'type': 'string', 'format': 'uri', 'minLength': 1, 'maxLength': 2 ** 16}),
745        (
746            stricturl(min_length=5, max_length=10),
747            {'title': 'A', 'type': 'string', 'format': 'uri', 'minLength': 5, 'maxLength': 10},
748        ),
749    ],
750)
751def test_special_str_types(field_type, expected_schema):
752    class Model(BaseModel):
753        a: field_type
754
755    base_schema = {'title': 'Model', 'type': 'object', 'properties': {'a': {}}, 'required': ['a']}
756    base_schema['properties']['a'] = expected_schema
757
758    assert Model.schema() == base_schema
759
760
761@pytest.mark.skipif(not email_validator, reason='email_validator not installed')
762@pytest.mark.parametrize('field_type,expected_schema', [(EmailStr, 'email'), (NameEmail, 'name-email')])
763def test_email_str_types(field_type, expected_schema):
764    class Model(BaseModel):
765        a: field_type
766
767    base_schema = {
768        'title': 'Model',
769        'type': 'object',
770        'properties': {'a': {'title': 'A', 'type': 'string'}},
771        'required': ['a'],
772    }
773    base_schema['properties']['a']['format'] = expected_schema
774
775    assert Model.schema() == base_schema
776
777
778@pytest.mark.parametrize('field_type,inner_type', [(SecretBytes, 'string'), (SecretStr, 'string')])
779def test_secret_types(field_type, inner_type):
780    class Model(BaseModel):
781        a: field_type
782
783    base_schema = {
784        'title': 'Model',
785        'type': 'object',
786        'properties': {'a': {'title': 'A', 'type': inner_type, 'writeOnly': True, 'format': 'password'}},
787        'required': ['a'],
788    }
789
790    assert Model.schema() == base_schema
791
792
793@pytest.mark.parametrize(
794    'field_type,expected_schema',
795    [
796        (ConstrainedInt, {}),
797        (conint(gt=5, lt=10), {'exclusiveMinimum': 5, 'exclusiveMaximum': 10}),
798        (conint(ge=5, le=10), {'minimum': 5, 'maximum': 10}),
799        (conint(multiple_of=5), {'multipleOf': 5}),
800        (PositiveInt, {'exclusiveMinimum': 0}),
801        (NegativeInt, {'exclusiveMaximum': 0}),
802        (NonNegativeInt, {'minimum': 0}),
803        (NonPositiveInt, {'maximum': 0}),
804    ],
805)
806def test_special_int_types(field_type, expected_schema):
807    class Model(BaseModel):
808        a: field_type
809
810    base_schema = {
811        'title': 'Model',
812        'type': 'object',
813        'properties': {'a': {'title': 'A', 'type': 'integer'}},
814        'required': ['a'],
815    }
816    base_schema['properties']['a'].update(expected_schema)
817
818    assert Model.schema() == base_schema
819
820
821@pytest.mark.parametrize(
822    'field_type,expected_schema',
823    [
824        (ConstrainedFloat, {}),
825        (confloat(gt=5, lt=10), {'exclusiveMinimum': 5, 'exclusiveMaximum': 10}),
826        (confloat(ge=5, le=10), {'minimum': 5, 'maximum': 10}),
827        (confloat(multiple_of=5), {'multipleOf': 5}),
828        (PositiveFloat, {'exclusiveMinimum': 0}),
829        (NegativeFloat, {'exclusiveMaximum': 0}),
830        (NonNegativeFloat, {'minimum': 0}),
831        (NonPositiveFloat, {'maximum': 0}),
832        (ConstrainedDecimal, {}),
833        (condecimal(gt=5, lt=10), {'exclusiveMinimum': 5, 'exclusiveMaximum': 10}),
834        (condecimal(ge=5, le=10), {'minimum': 5, 'maximum': 10}),
835        (condecimal(multiple_of=5), {'multipleOf': 5}),
836    ],
837)
838def test_special_float_types(field_type, expected_schema):
839    class Model(BaseModel):
840        a: field_type
841
842    base_schema = {
843        'title': 'Model',
844        'type': 'object',
845        'properties': {'a': {'title': 'A', 'type': 'number'}},
846        'required': ['a'],
847    }
848    base_schema['properties']['a'].update(expected_schema)
849
850    assert Model.schema() == base_schema
851
852
853@pytest.mark.parametrize(
854    'field_type,expected_schema',
855    [(UUID, 'uuid'), (UUID1, 'uuid1'), (UUID3, 'uuid3'), (UUID4, 'uuid4'), (UUID5, 'uuid5')],
856)
857def test_uuid_types(field_type, expected_schema):
858    class Model(BaseModel):
859        a: field_type
860
861    base_schema = {
862        'title': 'Model',
863        'type': 'object',
864        'properties': {'a': {'title': 'A', 'type': 'string', 'format': ''}},
865        'required': ['a'],
866    }
867    base_schema['properties']['a']['format'] = expected_schema
868
869    assert Model.schema() == base_schema
870
871
872@pytest.mark.parametrize(
873    'field_type,expected_schema', [(FilePath, 'file-path'), (DirectoryPath, 'directory-path'), (Path, 'path')]
874)
875def test_path_types(field_type, expected_schema):
876    class Model(BaseModel):
877        a: field_type
878
879    base_schema = {
880        'title': 'Model',
881        'type': 'object',
882        'properties': {'a': {'title': 'A', 'type': 'string', 'format': ''}},
883        'required': ['a'],
884    }
885    base_schema['properties']['a']['format'] = expected_schema
886
887    assert Model.schema() == base_schema
888
889
890def test_json_type():
891    class Model(BaseModel):
892        a: Json
893        b: Json[int]
894
895    assert Model.schema() == {
896        'title': 'Model',
897        'type': 'object',
898        'properties': {
899            'a': {'title': 'A', 'type': 'string', 'format': 'json-string'},
900            'b': {'title': 'B', 'type': 'integer'},
901        },
902        'required': ['b'],
903    }
904
905
906def test_ipv4address_type():
907    class Model(BaseModel):
908        ip_address: IPv4Address
909
910    model_schema = Model.schema()
911    assert model_schema == {
912        'title': 'Model',
913        'type': 'object',
914        'properties': {'ip_address': {'title': 'Ip Address', 'type': 'string', 'format': 'ipv4'}},
915        'required': ['ip_address'],
916    }
917
918
919def test_ipv6address_type():
920    class Model(BaseModel):
921        ip_address: IPv6Address
922
923    model_schema = Model.schema()
924    assert model_schema == {
925        'title': 'Model',
926        'type': 'object',
927        'properties': {'ip_address': {'title': 'Ip Address', 'type': 'string', 'format': 'ipv6'}},
928        'required': ['ip_address'],
929    }
930
931
932def test_ipvanyaddress_type():
933    class Model(BaseModel):
934        ip_address: IPvAnyAddress
935
936    model_schema = Model.schema()
937    assert model_schema == {
938        'title': 'Model',
939        'type': 'object',
940        'properties': {'ip_address': {'title': 'Ip Address', 'type': 'string', 'format': 'ipvanyaddress'}},
941        'required': ['ip_address'],
942    }
943
944
945def test_ipv4interface_type():
946    class Model(BaseModel):
947        ip_interface: IPv4Interface
948
949    model_schema = Model.schema()
950    assert model_schema == {
951        'title': 'Model',
952        'type': 'object',
953        'properties': {'ip_interface': {'title': 'Ip Interface', 'type': 'string', 'format': 'ipv4interface'}},
954        'required': ['ip_interface'],
955    }
956
957
958def test_ipv6interface_type():
959    class Model(BaseModel):
960        ip_interface: IPv6Interface
961
962    model_schema = Model.schema()
963    assert model_schema == {
964        'title': 'Model',
965        'type': 'object',
966        'properties': {'ip_interface': {'title': 'Ip Interface', 'type': 'string', 'format': 'ipv6interface'}},
967        'required': ['ip_interface'],
968    }
969
970
971def test_ipvanyinterface_type():
972    class Model(BaseModel):
973        ip_interface: IPvAnyInterface
974
975    model_schema = Model.schema()
976    assert model_schema == {
977        'title': 'Model',
978        'type': 'object',
979        'properties': {'ip_interface': {'title': 'Ip Interface', 'type': 'string', 'format': 'ipvanyinterface'}},
980        'required': ['ip_interface'],
981    }
982
983
984def test_ipv4network_type():
985    class Model(BaseModel):
986        ip_network: IPv4Network
987
988    model_schema = Model.schema()
989    assert model_schema == {
990        'title': 'Model',
991        'type': 'object',
992        'properties': {'ip_network': {'title': 'Ip Network', 'type': 'string', 'format': 'ipv4network'}},
993        'required': ['ip_network'],
994    }
995
996
997def test_ipv6network_type():
998    class Model(BaseModel):
999        ip_network: IPv6Network
1000
1001    model_schema = Model.schema()
1002    assert model_schema == {
1003        'title': 'Model',
1004        'type': 'object',
1005        'properties': {'ip_network': {'title': 'Ip Network', 'type': 'string', 'format': 'ipv6network'}},
1006        'required': ['ip_network'],
1007    }
1008
1009
1010def test_ipvanynetwork_type():
1011    class Model(BaseModel):
1012        ip_network: IPvAnyNetwork
1013
1014    model_schema = Model.schema()
1015    assert model_schema == {
1016        'title': 'Model',
1017        'type': 'object',
1018        'properties': {'ip_network': {'title': 'Ip Network', 'type': 'string', 'format': 'ipvanynetwork'}},
1019        'required': ['ip_network'],
1020    }
1021
1022
1023@pytest.mark.parametrize(
1024    'type_,default_value',
1025    (
1026        (Callable, ...),
1027        (Callable, lambda x: x),
1028        (Callable[[int], int], ...),
1029        (Callable[[int], int], lambda x: x),
1030    ),
1031)
1032def test_callable_type(type_, default_value):
1033    class Model(BaseModel):
1034        callback: type_ = default_value
1035        foo: int
1036
1037    with pytest.warns(UserWarning):
1038        model_schema = Model.schema()
1039
1040    assert 'callback' not in model_schema['properties']
1041
1042
1043def test_error_non_supported_types():
1044    class Model(BaseModel):
1045        a: PyObject
1046
1047    with pytest.raises(ValueError):
1048        Model.schema()
1049
1050
1051def create_testing_submodules():
1052    base_path = Path(tempfile.mkdtemp())
1053    mod_root_path = base_path / 'pydantic_schema_test'
1054    os.makedirs(mod_root_path, exist_ok=True)
1055    open(mod_root_path / '__init__.py', 'w').close()
1056    for mod in ['a', 'b', 'c']:
1057        module_name = 'module' + mod
1058        model_name = 'model' + mod + '.py'
1059        os.makedirs(mod_root_path / module_name, exist_ok=True)
1060        open(mod_root_path / module_name / '__init__.py', 'w').close()
1061        with open(mod_root_path / module_name / model_name, 'w') as f:
1062            f.write('from pydantic import BaseModel\n' 'class Model(BaseModel):\n' '    a: str\n')
1063    module_name = 'moduled'
1064    model_name = 'modeld.py'
1065    os.makedirs(mod_root_path / module_name, exist_ok=True)
1066    open(mod_root_path / module_name / '__init__.py', 'w').close()
1067    with open(mod_root_path / module_name / model_name, 'w') as f:
1068        f.write('from ..moduleb.modelb import Model')
1069    sys.path.insert(0, str(base_path))
1070
1071
1072def test_flat_models_unique_models():
1073    create_testing_submodules()
1074    from pydantic_schema_test.modulea.modela import Model as ModelA
1075    from pydantic_schema_test.moduleb.modelb import Model as ModelB
1076    from pydantic_schema_test.moduled.modeld import Model as ModelD
1077
1078    flat_models = get_flat_models_from_models([ModelA, ModelB, ModelD])
1079    assert flat_models == set([ModelA, ModelB])
1080
1081
1082def test_flat_models_with_submodels():
1083    class Foo(BaseModel):
1084        a: str
1085
1086    class Bar(BaseModel):
1087        b: List[Foo]
1088
1089    class Baz(BaseModel):
1090        c: Dict[str, Bar]
1091
1092    flat_models = get_flat_models_from_model(Baz)
1093    assert flat_models == set([Foo, Bar, Baz])
1094
1095
1096def test_flat_models_with_submodels_from_sequence():
1097    class Foo(BaseModel):
1098        a: str
1099
1100    class Bar(BaseModel):
1101        b: Foo
1102
1103    class Ingredient(BaseModel):
1104        name: str
1105
1106    class Pizza(BaseModel):
1107        name: str
1108        ingredients: List[Ingredient]
1109
1110    flat_models = get_flat_models_from_models([Bar, Pizza])
1111    assert flat_models == set([Foo, Bar, Ingredient, Pizza])
1112
1113
1114def test_model_name_maps():
1115    create_testing_submodules()
1116    from pydantic_schema_test.modulea.modela import Model as ModelA
1117    from pydantic_schema_test.moduleb.modelb import Model as ModelB
1118    from pydantic_schema_test.modulec.modelc import Model as ModelC
1119    from pydantic_schema_test.moduled.modeld import Model as ModelD
1120
1121    class Foo(BaseModel):
1122        a: str
1123
1124    class Bar(BaseModel):
1125        b: Foo
1126
1127    class Baz(BaseModel):
1128        c: Bar
1129
1130    flat_models = get_flat_models_from_models([Baz, ModelA, ModelB, ModelC, ModelD])
1131    model_name_map = get_model_name_map(flat_models)
1132    assert model_name_map == {
1133        Foo: 'Foo',
1134        Bar: 'Bar',
1135        Baz: 'Baz',
1136        ModelA: 'pydantic_schema_test__modulea__modela__Model',
1137        ModelB: 'pydantic_schema_test__moduleb__modelb__Model',
1138        ModelC: 'pydantic_schema_test__modulec__modelc__Model',
1139    }
1140
1141
1142def test_schema_overrides():
1143    class Foo(BaseModel):
1144        a: str
1145
1146    class Bar(BaseModel):
1147        b: Foo = Foo(a='foo')
1148
1149    class Baz(BaseModel):
1150        c: Optional[Bar]
1151
1152    class Model(BaseModel):
1153        d: Baz
1154
1155    model_schema = Model.schema()
1156    assert model_schema == {
1157        'title': 'Model',
1158        'type': 'object',
1159        'definitions': {
1160            'Foo': {
1161                'title': 'Foo',
1162                'type': 'object',
1163                'properties': {'a': {'title': 'A', 'type': 'string'}},
1164                'required': ['a'],
1165            },
1166            'Bar': {
1167                'title': 'Bar',
1168                'type': 'object',
1169                'properties': {'b': {'title': 'B', 'default': {'a': 'foo'}, 'allOf': [{'$ref': '#/definitions/Foo'}]}},
1170            },
1171            'Baz': {'title': 'Baz', 'type': 'object', 'properties': {'c': {'$ref': '#/definitions/Bar'}}},
1172        },
1173        'properties': {'d': {'$ref': '#/definitions/Baz'}},
1174        'required': ['d'],
1175    }
1176
1177
1178def test_schema_overrides_w_union():
1179    class Foo(BaseModel):
1180        pass
1181
1182    class Bar(BaseModel):
1183        pass
1184
1185    class Spam(BaseModel):
1186        a: Union[Foo, Bar] = Field(..., description='xxx')
1187
1188    assert Spam.schema()['properties'] == {
1189        'a': {
1190            'title': 'A',
1191            'description': 'xxx',
1192            'anyOf': [{'$ref': '#/definitions/Foo'}, {'$ref': '#/definitions/Bar'}],
1193        },
1194    }
1195
1196
1197def test_schema_from_models():
1198    class Foo(BaseModel):
1199        a: str
1200
1201    class Bar(BaseModel):
1202        b: Foo
1203
1204    class Baz(BaseModel):
1205        c: Bar
1206
1207    class Model(BaseModel):
1208        d: Baz
1209
1210    class Ingredient(BaseModel):
1211        name: str
1212
1213    class Pizza(BaseModel):
1214        name: str
1215        ingredients: List[Ingredient]
1216
1217    model_schema = schema(
1218        [Model, Pizza], title='Multi-model schema', description='Single JSON Schema with multiple definitions'
1219    )
1220    assert model_schema == {
1221        'title': 'Multi-model schema',
1222        'description': 'Single JSON Schema with multiple definitions',
1223        'definitions': {
1224            'Pizza': {
1225                'title': 'Pizza',
1226                'type': 'object',
1227                'properties': {
1228                    'name': {'title': 'Name', 'type': 'string'},
1229                    'ingredients': {
1230                        'title': 'Ingredients',
1231                        'type': 'array',
1232                        'items': {'$ref': '#/definitions/Ingredient'},
1233                    },
1234                },
1235                'required': ['name', 'ingredients'],
1236            },
1237            'Ingredient': {
1238                'title': 'Ingredient',
1239                'type': 'object',
1240                'properties': {'name': {'title': 'Name', 'type': 'string'}},
1241                'required': ['name'],
1242            },
1243            'Model': {
1244                'title': 'Model',
1245                'type': 'object',
1246                'properties': {'d': {'$ref': '#/definitions/Baz'}},
1247                'required': ['d'],
1248            },
1249            'Baz': {
1250                'title': 'Baz',
1251                'type': 'object',
1252                'properties': {'c': {'$ref': '#/definitions/Bar'}},
1253                'required': ['c'],
1254            },
1255            'Bar': {
1256                'title': 'Bar',
1257                'type': 'object',
1258                'properties': {'b': {'$ref': '#/definitions/Foo'}},
1259                'required': ['b'],
1260            },
1261            'Foo': {
1262                'title': 'Foo',
1263                'type': 'object',
1264                'properties': {'a': {'title': 'A', 'type': 'string'}},
1265                'required': ['a'],
1266            },
1267        },
1268    }
1269
1270
1271@pytest.mark.parametrize(
1272    'ref_prefix,ref_template',
1273    [
1274        # OpenAPI style
1275        ('#/components/schemas/', None),
1276        (None, '#/components/schemas/{model}'),
1277        # ref_prefix takes priority
1278        ('#/components/schemas/', '#/{model}/schemas/'),
1279    ],
1280)
1281def test_schema_with_refs(ref_prefix, ref_template):
1282    class Foo(BaseModel):
1283        a: str
1284
1285    class Bar(BaseModel):
1286        b: Foo
1287
1288    class Baz(BaseModel):
1289        c: Bar
1290
1291    model_schema = schema([Bar, Baz], ref_prefix=ref_prefix, ref_template=ref_template)
1292    assert model_schema == {
1293        'definitions': {
1294            'Baz': {
1295                'title': 'Baz',
1296                'type': 'object',
1297                'properties': {'c': {'$ref': '#/components/schemas/Bar'}},
1298                'required': ['c'],
1299            },
1300            'Bar': {
1301                'title': 'Bar',
1302                'type': 'object',
1303                'properties': {'b': {'$ref': '#/components/schemas/Foo'}},
1304                'required': ['b'],
1305            },
1306            'Foo': {
1307                'title': 'Foo',
1308                'type': 'object',
1309                'properties': {'a': {'title': 'A', 'type': 'string'}},
1310                'required': ['a'],
1311            },
1312        }
1313    }
1314
1315
1316def test_schema_with_custom_ref_template():
1317    class Foo(BaseModel):
1318        a: str
1319
1320    class Bar(BaseModel):
1321        b: Foo
1322
1323    class Baz(BaseModel):
1324        c: Bar
1325
1326    model_schema = schema([Bar, Baz], ref_template='/schemas/{model}.json#/')
1327    assert model_schema == {
1328        'definitions': {
1329            'Baz': {
1330                'title': 'Baz',
1331                'type': 'object',
1332                'properties': {'c': {'$ref': '/schemas/Bar.json#/'}},
1333                'required': ['c'],
1334            },
1335            'Bar': {
1336                'title': 'Bar',
1337                'type': 'object',
1338                'properties': {'b': {'$ref': '/schemas/Foo.json#/'}},
1339                'required': ['b'],
1340            },
1341            'Foo': {
1342                'title': 'Foo',
1343                'type': 'object',
1344                'properties': {'a': {'title': 'A', 'type': 'string'}},
1345                'required': ['a'],
1346            },
1347        }
1348    }
1349
1350
1351def test_schema_ref_template_key_error():
1352    class Foo(BaseModel):
1353        a: str
1354
1355    class Bar(BaseModel):
1356        b: Foo
1357
1358    class Baz(BaseModel):
1359        c: Bar
1360
1361    with pytest.raises(KeyError):
1362        schema([Bar, Baz], ref_template='/schemas/{bad_name}.json#/')
1363
1364
1365def test_schema_no_definitions():
1366    model_schema = schema([], title='Schema without definitions')
1367    assert model_schema == {'title': 'Schema without definitions'}
1368
1369
1370def test_list_default():
1371    class UserModel(BaseModel):
1372        friends: List[int] = [1]
1373
1374    assert UserModel.schema() == {
1375        'title': 'UserModel',
1376        'type': 'object',
1377        'properties': {'friends': {'title': 'Friends', 'default': [1], 'type': 'array', 'items': {'type': 'integer'}}},
1378    }
1379
1380
1381def test_dict_default():
1382    class UserModel(BaseModel):
1383        friends: Dict[str, float] = {'a': 1.1, 'b': 2.2}
1384
1385    assert UserModel.schema() == {
1386        'title': 'UserModel',
1387        'type': 'object',
1388        'properties': {
1389            'friends': {
1390                'title': 'Friends',
1391                'default': {'a': 1.1, 'b': 2.2},
1392                'type': 'object',
1393                'additionalProperties': {'type': 'number'},
1394            }
1395        },
1396    }
1397
1398
1399@pytest.mark.parametrize(
1400    'kwargs,type_,expected_extra',
1401    [
1402        ({'max_length': 5}, str, {'type': 'string', 'maxLength': 5}),
1403        ({}, constr(max_length=6), {'type': 'string', 'maxLength': 6}),
1404        ({'min_length': 2}, str, {'type': 'string', 'minLength': 2}),
1405        ({'max_length': 5}, bytes, {'type': 'string', 'maxLength': 5, 'format': 'binary'}),
1406        ({'regex': '^foo$'}, str, {'type': 'string', 'pattern': '^foo$'}),
1407        ({'gt': 2}, int, {'type': 'integer', 'exclusiveMinimum': 2}),
1408        ({'lt': 5}, int, {'type': 'integer', 'exclusiveMaximum': 5}),
1409        ({'ge': 2}, int, {'type': 'integer', 'minimum': 2}),
1410        ({'le': 5}, int, {'type': 'integer', 'maximum': 5}),
1411        ({'multiple_of': 5}, int, {'type': 'integer', 'multipleOf': 5}),
1412        ({'gt': 2}, float, {'type': 'number', 'exclusiveMinimum': 2}),
1413        ({'lt': 5}, float, {'type': 'number', 'exclusiveMaximum': 5}),
1414        ({'ge': 2}, float, {'type': 'number', 'minimum': 2}),
1415        ({'le': 5}, float, {'type': 'number', 'maximum': 5}),
1416        ({'gt': -math.inf}, float, {'type': 'number'}),
1417        ({'lt': math.inf}, float, {'type': 'number'}),
1418        ({'ge': -math.inf}, float, {'type': 'number'}),
1419        ({'le': math.inf}, float, {'type': 'number'}),
1420        ({'multiple_of': 5}, float, {'type': 'number', 'multipleOf': 5}),
1421        ({'gt': 2}, Decimal, {'type': 'number', 'exclusiveMinimum': 2}),
1422        ({'lt': 5}, Decimal, {'type': 'number', 'exclusiveMaximum': 5}),
1423        ({'ge': 2}, Decimal, {'type': 'number', 'minimum': 2}),
1424        ({'le': 5}, Decimal, {'type': 'number', 'maximum': 5}),
1425        ({'multiple_of': 5}, Decimal, {'type': 'number', 'multipleOf': 5}),
1426    ],
1427)
1428def test_constraints_schema(kwargs, type_, expected_extra):
1429    class Foo(BaseModel):
1430        a: type_ = Field('foo', title='A title', description='A description', **kwargs)
1431
1432    expected_schema = {
1433        'title': 'Foo',
1434        'type': 'object',
1435        'properties': {'a': {'title': 'A title', 'description': 'A description', 'default': 'foo'}},
1436    }
1437
1438    expected_schema['properties']['a'].update(expected_extra)
1439    assert Foo.schema() == expected_schema
1440
1441
1442@pytest.mark.parametrize(
1443    'kwargs,type_',
1444    [
1445        ({'max_length': 5}, int),
1446        ({'min_length': 2}, float),
1447        ({'max_length': 5}, Decimal),
1448        ({'allow_mutation': False}, bool),
1449        ({'regex': '^foo$'}, int),
1450        ({'gt': 2}, str),
1451        ({'lt': 5}, bytes),
1452        ({'ge': 2}, str),
1453        ({'le': 5}, bool),
1454        ({'gt': 0}, Callable),
1455        ({'gt': 0}, Callable[[int], int]),
1456        ({'gt': 0}, conlist(int, min_items=4)),
1457        ({'gt': 0}, conset(int, min_items=4)),
1458    ],
1459)
1460def test_unenforced_constraints_schema(kwargs, type_):
1461    with pytest.raises(ValueError, match='On field "a" the following field constraints are set but not enforced'):
1462
1463        class Foo(BaseModel):
1464            a: type_ = Field('foo', title='A title', description='A description', **kwargs)
1465
1466
1467@pytest.mark.parametrize(
1468    'kwargs,type_,value',
1469    [
1470        ({'max_length': 5}, str, 'foo'),
1471        ({'min_length': 2}, str, 'foo'),
1472        ({'max_length': 5}, bytes, b'foo'),
1473        ({'regex': '^foo$'}, str, 'foo'),
1474        ({'gt': 2}, int, 3),
1475        ({'lt': 5}, int, 3),
1476        ({'ge': 2}, int, 3),
1477        ({'ge': 2}, int, 2),
1478        ({'gt': 2}, int, '3'),
1479        ({'le': 5}, int, 3),
1480        ({'le': 5}, int, 5),
1481        ({'gt': 2}, float, 3.0),
1482        ({'gt': 2}, float, 2.1),
1483        ({'lt': 5}, float, 3.0),
1484        ({'lt': 5}, float, 4.9),
1485        ({'ge': 2}, float, 3.0),
1486        ({'ge': 2}, float, 2.0),
1487        ({'le': 5}, float, 3.0),
1488        ({'le': 5}, float, 5.0),
1489        ({'gt': 2}, float, 3),
1490        ({'gt': 2}, float, '3'),
1491        ({'gt': 2}, Decimal, Decimal(3)),
1492        ({'lt': 5}, Decimal, Decimal(3)),
1493        ({'ge': 2}, Decimal, Decimal(3)),
1494        ({'ge': 2}, Decimal, Decimal(2)),
1495        ({'le': 5}, Decimal, Decimal(3)),
1496        ({'le': 5}, Decimal, Decimal(5)),
1497    ],
1498)
1499def test_constraints_schema_validation(kwargs, type_, value):
1500    class Foo(BaseModel):
1501        a: type_ = Field('foo', title='A title', description='A description', **kwargs)
1502
1503    assert Foo(a=value)
1504
1505
1506@pytest.mark.parametrize(
1507    'kwargs,type_,value',
1508    [
1509        ({'max_length': 5}, str, 'foobar'),
1510        ({'min_length': 2}, str, 'f'),
1511        ({'regex': '^foo$'}, str, 'bar'),
1512        ({'gt': 2}, int, 2),
1513        ({'lt': 5}, int, 5),
1514        ({'ge': 2}, int, 1),
1515        ({'le': 5}, int, 6),
1516        ({'gt': 2}, float, 2.0),
1517        ({'lt': 5}, float, 5.0),
1518        ({'ge': 2}, float, 1.9),
1519        ({'le': 5}, float, 5.1),
1520        ({'gt': 2}, Decimal, Decimal(2)),
1521        ({'lt': 5}, Decimal, Decimal(5)),
1522        ({'ge': 2}, Decimal, Decimal(1)),
1523        ({'le': 5}, Decimal, Decimal(6)),
1524    ],
1525)
1526def test_constraints_schema_validation_raises(kwargs, type_, value):
1527    class Foo(BaseModel):
1528        a: type_ = Field('foo', title='A title', description='A description', **kwargs)
1529
1530    with pytest.raises(ValidationError):
1531        Foo(a=value)
1532
1533
1534def test_schema_kwargs():
1535    class Foo(BaseModel):
1536        a: str = Field('foo', examples=['bar'])
1537
1538    assert Foo.schema() == {
1539        'title': 'Foo',
1540        'type': 'object',
1541        'properties': {'a': {'type': 'string', 'title': 'A', 'default': 'foo', 'examples': ['bar']}},
1542    }
1543
1544
1545def test_schema_dict_constr():
1546    regex_str = r'^([a-zA-Z_][a-zA-Z0-9_]*)$'
1547    ConStrType = constr(regex=regex_str)
1548    ConStrKeyDict = Dict[ConStrType, str]
1549
1550    class Foo(BaseModel):
1551        a: ConStrKeyDict = {}
1552
1553    assert Foo.schema() == {
1554        'title': 'Foo',
1555        'type': 'object',
1556        'properties': {
1557            'a': {'type': 'object', 'title': 'A', 'default': {}, 'patternProperties': {regex_str: {'type': 'string'}}}
1558        },
1559    }
1560
1561
1562@pytest.mark.parametrize(
1563    'field_type,expected_schema',
1564    [
1565        (ConstrainedBytes, {'title': 'A', 'type': 'string', 'format': 'binary'}),
1566        (
1567            conbytes(min_length=3, max_length=5),
1568            {'title': 'A', 'type': 'string', 'format': 'binary', 'minLength': 3, 'maxLength': 5},
1569        ),
1570    ],
1571)
1572def test_bytes_constrained_types(field_type, expected_schema):
1573    class Model(BaseModel):
1574        a: field_type
1575
1576    base_schema = {'title': 'Model', 'type': 'object', 'properties': {'a': {}}, 'required': ['a']}
1577    base_schema['properties']['a'] = expected_schema
1578
1579    assert Model.schema() == base_schema
1580
1581
1582def test_optional_dict():
1583    class Model(BaseModel):
1584        something: Optional[Dict[str, Any]]
1585
1586    assert Model.schema() == {
1587        'title': 'Model',
1588        'type': 'object',
1589        'properties': {'something': {'title': 'Something', 'type': 'object'}},
1590    }
1591
1592    assert Model().dict() == {'something': None}
1593    assert Model(something={'foo': 'Bar'}).dict() == {'something': {'foo': 'Bar'}}
1594
1595
1596def test_optional_validator():
1597    class Model(BaseModel):
1598        something: Optional[str]
1599
1600        @validator('something', always=True)
1601        def check_something(cls, v):
1602            assert v is None or 'x' not in v, 'should not contain x'
1603            return v
1604
1605    assert Model.schema() == {
1606        'title': 'Model',
1607        'type': 'object',
1608        'properties': {'something': {'title': 'Something', 'type': 'string'}},
1609    }
1610
1611    assert Model().dict() == {'something': None}
1612    assert Model(something=None).dict() == {'something': None}
1613    assert Model(something='hello').dict() == {'something': 'hello'}
1614
1615
1616def test_field_with_validator():
1617    class Model(BaseModel):
1618        something: Optional[int] = None
1619
1620        @validator('something')
1621        def check_field(cls, v, *, values, config, field):
1622            return v
1623
1624    assert Model.schema() == {
1625        'title': 'Model',
1626        'type': 'object',
1627        'properties': {'something': {'type': 'integer', 'title': 'Something'}},
1628    }
1629
1630
1631def test_unparameterized_schema_generation():
1632    class FooList(BaseModel):
1633        d: List
1634
1635    class BarList(BaseModel):
1636        d: list
1637
1638    assert model_schema(FooList) == {
1639        'title': 'FooList',
1640        'type': 'object',
1641        'properties': {'d': {'items': {}, 'title': 'D', 'type': 'array'}},
1642        'required': ['d'],
1643    }
1644
1645    foo_list_schema = model_schema(FooList)
1646    bar_list_schema = model_schema(BarList)
1647    bar_list_schema['title'] = 'FooList'  # to check for equality
1648    assert foo_list_schema == bar_list_schema
1649
1650    class FooDict(BaseModel):
1651        d: Dict
1652
1653    class BarDict(BaseModel):
1654        d: dict
1655
1656    model_schema(Foo)
1657    assert model_schema(FooDict) == {
1658        'title': 'FooDict',
1659        'type': 'object',
1660        'properties': {'d': {'title': 'D', 'type': 'object'}},
1661        'required': ['d'],
1662    }
1663
1664    foo_dict_schema = model_schema(FooDict)
1665    bar_dict_schema = model_schema(BarDict)
1666    bar_dict_schema['title'] = 'FooDict'  # to check for equality
1667    assert foo_dict_schema == bar_dict_schema
1668
1669
1670def test_known_model_optimization():
1671    class Dep(BaseModel):
1672        number: int
1673
1674    class Model(BaseModel):
1675        dep: Dep
1676        dep_l: List[Dep]
1677
1678    expected = {
1679        'title': 'Model',
1680        'type': 'object',
1681        'properties': {
1682            'dep': {'$ref': '#/definitions/Dep'},
1683            'dep_l': {'title': 'Dep L', 'type': 'array', 'items': {'$ref': '#/definitions/Dep'}},
1684        },
1685        'required': ['dep', 'dep_l'],
1686        'definitions': {
1687            'Dep': {
1688                'title': 'Dep',
1689                'type': 'object',
1690                'properties': {'number': {'title': 'Number', 'type': 'integer'}},
1691                'required': ['number'],
1692            }
1693        },
1694    }
1695
1696    assert Model.schema() == expected
1697
1698
1699def test_root():
1700    class Model(BaseModel):
1701        __root__: str
1702
1703    assert Model.schema() == {'title': 'Model', 'type': 'string'}
1704
1705
1706def test_root_list():
1707    class Model(BaseModel):
1708        __root__: List[str]
1709
1710    assert Model.schema() == {'title': 'Model', 'type': 'array', 'items': {'type': 'string'}}
1711
1712
1713def test_root_nested_model():
1714    class NestedModel(BaseModel):
1715        a: str
1716
1717    class Model(BaseModel):
1718        __root__: List[NestedModel]
1719
1720    assert Model.schema() == {
1721        'title': 'Model',
1722        'type': 'array',
1723        'items': {'$ref': '#/definitions/NestedModel'},
1724        'definitions': {
1725            'NestedModel': {
1726                'title': 'NestedModel',
1727                'type': 'object',
1728                'properties': {'a': {'title': 'A', 'type': 'string'}},
1729                'required': ['a'],
1730            }
1731        },
1732    }
1733
1734
1735def test_new_type_schema():
1736    a_type = NewType('a_type', int)
1737    b_type = NewType('b_type', a_type)
1738    c_type = NewType('c_type', str)
1739
1740    class Model(BaseModel):
1741        a: a_type
1742        b: b_type
1743        c: c_type
1744
1745    assert Model.schema() == {
1746        'properties': {
1747            'a': {'title': 'A', 'type': 'integer'},
1748            'b': {'title': 'B', 'type': 'integer'},
1749            'c': {'title': 'C', 'type': 'string'},
1750        },
1751        'required': ['a', 'b', 'c'],
1752        'title': 'Model',
1753        'type': 'object',
1754    }
1755
1756
1757def test_literal_schema():
1758    class Model(BaseModel):
1759        a: Literal[1]
1760        b: Literal['a']
1761        c: Literal['a', 1]
1762        d: Literal['a', Literal['b'], 1, 2]
1763
1764    assert Model.schema() == {
1765        'properties': {
1766            'a': {'title': 'A', 'type': 'integer', 'enum': [1]},
1767            'b': {'title': 'B', 'type': 'string', 'enum': ['a']},
1768            'c': {'title': 'C', 'anyOf': [{'type': 'string', 'enum': ['a']}, {'type': 'integer', 'enum': [1]}]},
1769            'd': {
1770                'title': 'D',
1771                'anyOf': [
1772                    {'type': 'string', 'enum': ['a', 'b']},
1773                    {'type': 'integer', 'enum': [1, 2]},
1774                ],
1775            },
1776        },
1777        'required': ['a', 'b', 'c', 'd'],
1778        'title': 'Model',
1779        'type': 'object',
1780    }
1781
1782
1783def test_literal_enum():
1784    class MyEnum(str, Enum):
1785        FOO = 'foo'
1786        BAR = 'bar'
1787
1788    class Model(BaseModel):
1789        kind: Literal[MyEnum.FOO]
1790
1791    assert Model.schema() == {
1792        'title': 'Model',
1793        'type': 'object',
1794        'properties': {'kind': {'title': 'Kind', 'enum': ['foo'], 'type': 'string'}},
1795        'required': ['kind'],
1796    }
1797
1798
1799def test_color_type():
1800    class Model(BaseModel):
1801        color: Color
1802
1803    model_schema = Model.schema()
1804    assert model_schema == {
1805        'title': 'Model',
1806        'type': 'object',
1807        'properties': {'color': {'title': 'Color', 'type': 'string', 'format': 'color'}},
1808        'required': ['color'],
1809    }
1810
1811
1812def test_model_with_schema_extra():
1813    class Model(BaseModel):
1814        a: str
1815
1816        class Config:
1817            schema_extra = {'examples': [{'a': 'Foo'}]}
1818
1819    assert Model.schema() == {
1820        'title': 'Model',
1821        'type': 'object',
1822        'properties': {'a': {'title': 'A', 'type': 'string'}},
1823        'required': ['a'],
1824        'examples': [{'a': 'Foo'}],
1825    }
1826
1827
1828def test_model_with_schema_extra_callable():
1829    class Model(BaseModel):
1830        name: str = None
1831
1832        class Config:
1833            @staticmethod
1834            def schema_extra(schema, model_class):
1835                schema.pop('properties')
1836                schema['type'] = 'override'
1837                assert model_class is Model
1838
1839    assert Model.schema() == {'title': 'Model', 'type': 'override'}
1840
1841
1842def test_model_with_schema_extra_callable_no_model_class():
1843    class Model(BaseModel):
1844        name: str = None
1845
1846        class Config:
1847            @staticmethod
1848            def schema_extra(schema):
1849                schema.pop('properties')
1850                schema['type'] = 'override'
1851
1852    assert Model.schema() == {'title': 'Model', 'type': 'override'}
1853
1854
1855def test_model_with_schema_extra_callable_classmethod():
1856    class Model(BaseModel):
1857        name: str = None
1858
1859        class Config:
1860            type = 'foo'
1861
1862            @classmethod
1863            def schema_extra(cls, schema, model_class):
1864                schema.pop('properties')
1865                schema['type'] = cls.type
1866                assert model_class is Model
1867
1868    assert Model.schema() == {'title': 'Model', 'type': 'foo'}
1869
1870
1871def test_model_with_schema_extra_callable_instance_method():
1872    class Model(BaseModel):
1873        name: str = None
1874
1875        class Config:
1876            def schema_extra(schema, model_class):
1877                schema.pop('properties')
1878                schema['type'] = 'override'
1879                assert model_class is Model
1880
1881    assert Model.schema() == {'title': 'Model', 'type': 'override'}
1882
1883
1884def test_model_with_extra_forbidden():
1885    class Model(BaseModel):
1886        a: str
1887
1888        class Config:
1889            extra = Extra.forbid
1890
1891    assert Model.schema() == {
1892        'title': 'Model',
1893        'type': 'object',
1894        'properties': {'a': {'title': 'A', 'type': 'string'}},
1895        'required': ['a'],
1896        'additionalProperties': False,
1897    }
1898
1899
1900@pytest.mark.parametrize(
1901    'annotation,kwargs,field_schema',
1902    [
1903        (int, dict(gt=0), {'title': 'A', 'exclusiveMinimum': 0, 'type': 'integer'}),
1904        (Optional[int], dict(gt=0), {'title': 'A', 'exclusiveMinimum': 0, 'type': 'integer'}),
1905        (
1906            Tuple[int, ...],
1907            dict(gt=0),
1908            {'title': 'A', 'exclusiveMinimum': 0, 'type': 'array', 'items': {'exclusiveMinimum': 0, 'type': 'integer'}},
1909        ),
1910        (
1911            Tuple[int, int, int],
1912            dict(gt=0),
1913            {
1914                'title': 'A',
1915                'type': 'array',
1916                'items': [
1917                    {'exclusiveMinimum': 0, 'type': 'integer'},
1918                    {'exclusiveMinimum': 0, 'type': 'integer'},
1919                    {'exclusiveMinimum': 0, 'type': 'integer'},
1920                ],
1921            },
1922        ),
1923        (
1924            Union[int, float],
1925            dict(gt=0),
1926            {
1927                'title': 'A',
1928                'anyOf': [{'exclusiveMinimum': 0, 'type': 'integer'}, {'exclusiveMinimum': 0, 'type': 'number'}],
1929            },
1930        ),
1931        (
1932            List[int],
1933            dict(gt=0),
1934            {'title': 'A', 'exclusiveMinimum': 0, 'type': 'array', 'items': {'exclusiveMinimum': 0, 'type': 'integer'}},
1935        ),
1936        (
1937            Dict[str, int],
1938            dict(gt=0),
1939            {
1940                'title': 'A',
1941                'exclusiveMinimum': 0,
1942                'type': 'object',
1943                'additionalProperties': {'exclusiveMinimum': 0, 'type': 'integer'},
1944            },
1945        ),
1946        (
1947            Union[str, int],
1948            dict(gt=0, max_length=5),
1949            {'title': 'A', 'anyOf': [{'maxLength': 5, 'type': 'string'}, {'exclusiveMinimum': 0, 'type': 'integer'}]},
1950        ),
1951    ],
1952)
1953def test_enforced_constraints(annotation, kwargs, field_schema):
1954    class Model(BaseModel):
1955        a: annotation = Field(..., **kwargs)
1956
1957    schema = Model.schema()
1958    # debug(schema['properties']['a'])
1959    assert schema['properties']['a'] == field_schema
1960
1961
1962def test_real_vs_phony_constraints():
1963    class Model1(BaseModel):
1964        foo: int = Field(..., gt=123)
1965
1966        class Config:
1967            title = 'Test Model'
1968
1969    class Model2(BaseModel):
1970        foo: int = Field(..., exclusiveMinimum=123)
1971
1972        class Config:
1973            title = 'Test Model'
1974
1975    with pytest.raises(ValidationError, match='ensure this value is greater than 123'):
1976        Model1(foo=122)
1977
1978    assert Model2(foo=122).dict() == {'foo': 122}
1979
1980    assert (
1981        Model1.schema()
1982        == Model2.schema()
1983        == {
1984            'title': 'Test Model',
1985            'type': 'object',
1986            'properties': {'foo': {'title': 'Foo', 'exclusiveMinimum': 123, 'type': 'integer'}},
1987            'required': ['foo'],
1988        }
1989    )
1990
1991
1992def test_subfield_field_info():
1993    class MyModel(BaseModel):
1994        entries: Dict[str, List[int]]
1995
1996    assert MyModel.schema() == {
1997        'title': 'MyModel',
1998        'type': 'object',
1999        'properties': {
2000            'entries': {
2001                'title': 'Entries',
2002                'type': 'object',
2003                'additionalProperties': {'type': 'array', 'items': {'type': 'integer'}},
2004            }
2005        },
2006        'required': ['entries'],
2007    }
2008
2009
2010def test_dataclass():
2011    @dataclass
2012    class Model:
2013        a: bool
2014
2015    assert schema([Model]) == {
2016        'definitions': {
2017            'Model': {
2018                'title': 'Model',
2019                'type': 'object',
2020                'properties': {'a': {'title': 'A', 'type': 'boolean'}},
2021                'required': ['a'],
2022            }
2023        }
2024    }
2025
2026    assert model_schema(Model) == {
2027        'title': 'Model',
2028        'type': 'object',
2029        'properties': {'a': {'title': 'A', 'type': 'boolean'}},
2030        'required': ['a'],
2031    }
2032
2033
2034def test_schema_attributes():
2035    class ExampleEnum(Enum):
2036        """This is a test description."""
2037
2038        gt = 'GT'
2039        lt = 'LT'
2040        ge = 'GE'
2041        le = 'LE'
2042        max_length = 'ML'
2043        multiple_of = 'MO'
2044        regex = 'RE'
2045
2046    class Example(BaseModel):
2047        example: ExampleEnum
2048
2049    assert Example.schema() == {
2050        'title': 'Example',
2051        'type': 'object',
2052        'properties': {'example': {'$ref': '#/definitions/ExampleEnum'}},
2053        'required': ['example'],
2054        'definitions': {
2055            'ExampleEnum': {
2056                'title': 'ExampleEnum',
2057                'description': 'This is a test description.',
2058                'enum': ['GT', 'LT', 'GE', 'LE', 'ML', 'MO', 'RE'],
2059            }
2060        },
2061    }
2062
2063
2064def test_model_process_schema_enum():
2065    class SpamEnum(str, Enum):
2066        foo = 'f'
2067        bar = 'b'
2068
2069    model_schema, _, _ = model_process_schema(SpamEnum, model_name_map={})
2070    assert model_schema == {'title': 'SpamEnum', 'description': 'An enumeration.', 'type': 'string', 'enum': ['f', 'b']}
2071
2072
2073def test_path_modify_schema():
2074    class MyPath(Path):
2075        @classmethod
2076        def __modify_schema__(cls, schema):
2077            schema.update(foobar=123)
2078
2079    class Model(BaseModel):
2080        path1: Path
2081        path2: MyPath
2082        path3: List[MyPath]
2083
2084    assert Model.schema() == {
2085        'title': 'Model',
2086        'type': 'object',
2087        'properties': {
2088            'path1': {'title': 'Path1', 'type': 'string', 'format': 'path'},
2089            'path2': {'title': 'Path2', 'type': 'string', 'format': 'path', 'foobar': 123},
2090            'path3': {'title': 'Path3', 'type': 'array', 'items': {'type': 'string', 'format': 'path', 'foobar': 123}},
2091        },
2092        'required': ['path1', 'path2', 'path3'],
2093    }
2094
2095
2096def test_frozen_set():
2097    class Model(BaseModel):
2098        a: FrozenSet[int] = frozenset({1, 2, 3})
2099        b: FrozenSet = frozenset({1, 2, 3})
2100        c: frozenset = frozenset({1, 2, 3})
2101        d: frozenset = ...
2102
2103    assert Model.schema() == {
2104        'title': 'Model',
2105        'type': 'object',
2106        'properties': {
2107            'a': {
2108                'title': 'A',
2109                'default': frozenset({1, 2, 3}),
2110                'type': 'array',
2111                'items': {'type': 'integer'},
2112                'uniqueItems': True,
2113            },
2114            'b': {'title': 'B', 'default': frozenset({1, 2, 3}), 'type': 'array', 'items': {}, 'uniqueItems': True},
2115            'c': {'title': 'C', 'default': frozenset({1, 2, 3}), 'type': 'array', 'items': {}, 'uniqueItems': True},
2116            'd': {'title': 'D', 'type': 'array', 'items': {}, 'uniqueItems': True},
2117        },
2118        'required': ['d'],
2119    }
2120
2121
2122def test_iterable():
2123    class Model(BaseModel):
2124        a: Iterable[int]
2125
2126    assert Model.schema() == {
2127        'title': 'Model',
2128        'type': 'object',
2129        'properties': {'a': {'title': 'A', 'type': 'array', 'items': {'type': 'integer'}}},
2130        'required': ['a'],
2131    }
2132
2133
2134def test_new_type():
2135    new_type = NewType('NewStr', str)
2136
2137    class Model(BaseModel):
2138        a: new_type
2139
2140    assert Model.schema() == {
2141        'title': 'Model',
2142        'type': 'object',
2143        'properties': {'a': {'title': 'A', 'type': 'string'}},
2144        'required': ['a'],
2145    }
2146
2147
2148def test_multiple_models_with_same_name(create_module):
2149    module = create_module(
2150        # language=Python
2151        """
2152from pydantic import BaseModel
2153
2154
2155class ModelOne(BaseModel):
2156    class NestedModel(BaseModel):
2157        a: float
2158
2159    nested: NestedModel
2160
2161
2162class ModelTwo(BaseModel):
2163    class NestedModel(BaseModel):
2164        b: float
2165
2166    nested: NestedModel
2167
2168
2169class NestedModel(BaseModel):
2170    c: float
2171        """
2172    )
2173
2174    models = [module.ModelOne, module.ModelTwo, module.NestedModel]
2175    model_names = set(schema(models)['definitions'].keys())
2176    expected_model_names = {
2177        'ModelOne',
2178        'ModelTwo',
2179        f'{module.__name__}__ModelOne__NestedModel',
2180        f'{module.__name__}__ModelTwo__NestedModel',
2181        f'{module.__name__}__NestedModel',
2182    }
2183    assert model_names == expected_model_names
2184
2185
2186def test_multiple_enums_with_same_name(create_module):
2187    module_1 = create_module(
2188        # language=Python
2189        """
2190from enum import Enum
2191
2192from pydantic import BaseModel
2193
2194
2195class MyEnum(str, Enum):
2196    a = 'a'
2197    b = 'b'
2198    c = 'c'
2199
2200
2201class MyModel(BaseModel):
2202    my_enum_1: MyEnum
2203        """
2204    )
2205
2206    module_2 = create_module(
2207        # language=Python
2208        """
2209from enum import Enum
2210
2211from pydantic import BaseModel
2212
2213
2214class MyEnum(str, Enum):
2215    d = 'd'
2216    e = 'e'
2217    f = 'f'
2218
2219
2220class MyModel(BaseModel):
2221    my_enum_2: MyEnum
2222        """
2223    )
2224
2225    class Model(BaseModel):
2226        my_model_1: module_1.MyModel
2227        my_model_2: module_2.MyModel
2228
2229    assert len(Model.schema()['definitions']) == 4
2230    assert set(Model.schema()['definitions']) == {
2231        f'{module_1.__name__}__MyEnum',
2232        f'{module_1.__name__}__MyModel',
2233        f'{module_2.__name__}__MyEnum',
2234        f'{module_2.__name__}__MyModel',
2235    }
2236
2237
2238@pytest.mark.skipif(
2239    sys.version_info < (3, 7), reason='schema generation for generic fields is not available in python < 3.7'
2240)
2241def test_schema_for_generic_field():
2242    T = TypeVar('T')
2243
2244    class GenModel(Generic[T]):
2245        def __init__(self, data: Any):
2246            self.data = data
2247
2248        @classmethod
2249        def __get_validators__(cls):
2250            yield cls.validate
2251
2252        @classmethod
2253        def validate(cls, v: Any):
2254            return v
2255
2256    class Model(BaseModel):
2257        data: GenModel[str]
2258        data1: GenModel
2259
2260    assert Model.schema() == {
2261        'title': 'Model',
2262        'type': 'object',
2263        'properties': {
2264            'data': {'title': 'Data', 'type': 'string'},
2265            'data1': {
2266                'title': 'Data1',
2267            },
2268        },
2269        'required': ['data', 'data1'],
2270    }
2271
2272    class GenModelModified(GenModel, Generic[T]):
2273        @classmethod
2274        def __modify_schema__(cls, field_schema):
2275            field_schema.pop('type', None)
2276            field_schema.update(anyOf=[{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}])
2277
2278    class ModelModified(BaseModel):
2279        data: GenModelModified[str]
2280        data1: GenModelModified
2281
2282    assert ModelModified.schema() == {
2283        'title': 'ModelModified',
2284        'type': 'object',
2285        'properties': {
2286            'data': {'title': 'Data', 'anyOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]},
2287            'data1': {'title': 'Data1', 'anyOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]},
2288        },
2289        'required': ['data', 'data1'],
2290    }
2291