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