Custom types and core schemas
Build Pydantic custom types and pydantic-core schemas. Python examples cover SchemaValidator, SchemaSerializer, context, unions, and tests.
Custom domain types and extension hooks#
Teach Pydantic how to validate, store, serialize, and document a type your application owns.
Start with Annotated, Field, and high-level validators. When you need a distinct runtime object or an integration for a third-party class, implement __get_pydantic_core_schema__. It receives the source type and a schema-generation handler. __get_pydantic_json_schema__ separately describes its external representation.
from dataclasses import dataclass
from pydantic import BaseModel, TypeAdapter, ValidationError
from pydantic_core import core_schema
PATTERN = r'^[0-9a-f]{64}$'
@dataclass(frozen=True)
class Digest:
hex: str
@classmethod
def __get_pydantic_core_schema__(cls, source_type, handler):
text_schema = core_schema.str_schema(strict=True, pattern=PATTERN)
from_text = core_schema.no_info_after_validator_function(cls, text_schema)
# Existing objects are rechecked too, rather than trusted blindly.
existing = core_schema.chain_schema([
core_schema.is_instance_schema(cls),
core_schema.no_info_plain_validator_function(lambda value: value.hex),
from_text,
])
return core_schema.json_or_python_schema(
json_schema=from_text,
python_schema=core_schema.union_schema([existing, from_text]),
serialization=core_schema.plain_serializer_function_ser_schema(
lambda value: value.hex,
return_schema=text_schema
)
)
@classmethod
def __get_pydantic_json_schema__(cls, schema, handler):
result = handler(core_schema.str_schema(pattern=PATTERN))
result['description'] = 'Lowercase SHA-256 digest, 64 hexadecimal characters'
return result
class Upload(BaseModel):
filename: str
digest: Digest
adapter = TypeAdapter(Digest)
digest = adapter.validate_json('"' + 'a' * 64 + '"')
upload = Upload(filename='paper.pdf', digest=digest)
print(type(upload.digest).__name__)
print(type(upload.model_dump()['digest']).__name__)
print(upload.model_dump_json())
print(adapter.json_schema()['pattern'])
assert adapter.validate_json(adapter.dump_json(digest)) == digest
for bad in ['short', Digest('broken')]:
try:
adapter.validate_python(bad)
except ValidationError:
print('Rejected:', type(bad).__name__)Digest
str
{"filename":"paper.pdf","digest":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
^[0-9a-f]{64}$
Rejected: str
Rejected: DigestThe string rule requires 64 lowercase hexadecimal characters before constructing Digest. Existing Python instances have their hex value checked again. Dumping returns that string. The frozen dataclass prevents reassignment, but its ordinary constructor does not validate the format.
This validates the digest’s format, not the contents of a file. Compute a file’s SHA-256 with your hashing code and compare it separately when integrity matters.
json_or_python_schema accepts a string in JSON and a string or existing object in Python. Validation constructs the domain object; serialization returns its hex value. The JSON Schema hook documents the string format.
from typing import Annotated
from pydantic import BaseModel, GetPydanticSchema
from pydantic_core import core_schema
def canonical_name_schema(source_type, handler):
return core_schema.no_info_after_validator_function(
str.casefold, handler(source_type)
)
CanonicalName = Annotated[str, GetPydanticSchema(canonical_name_schema)]
class SearchQuery(BaseModel):
author: CanonicalName
print(SearchQuery(author='MIRA').author)miraCalling handler(source_type) preserves the underlying type’s validation and other metadata. For an unrelated generic argument, use handler.generate_schema(item_type) to avoid applying the enclosing metadata to it. Calling the handler on an unsupported custom class can recurse or fail; build its schema or delegate to a supported underlying type. Core hooks are more sensitive to API changes than the high-level helpers, so pin compatible versions and test upgrades.
A schema hook runs when the model or adapter is prepared. Its handler(source_type) builds rules; a wrap validator’s handler(value) checks input at runtime. no_info callbacks receive a value, while with_info callbacks also receive validation information.
Official reference: Custom domain types and extension hooks.
pydantic-core: core_schema building blocks#
A core schema is an executable validation plan, not a public JSON Schema document.
core_schema contains helper functions that create schema dictionaries. They do not validate a value by themselves. SchemaValidator(schema) compiles the validation plan; SchemaSerializer(schema) compiles its output rules. Pydantic normally builds these plans from your type hints, so hand-author them only when your integration needs that level of control.
| Builder | Meaning |
|---|---|
str_schema / int_schema / decimal_schema | A primitive check with constraints |
list_schema / dict_schema / tuple_schema | Validate container shape and item values |
typed_dict_schema + typed_dict_field | Named dictionary fields and required/optional keys |
nullable_schema | Allow None in addition to an inner schema |
with_default_schema | Supply a default when a value is missing |
union_schema / tagged_union_schema | Try alternatives / pick a tagged alternative |
chain_schema | Feed each stage’s output into the next stage |
json_or_python_schema | Select the input-mode branch |
definitions_schema + definition_reference_schema | Shared or recursive schema nodes |
model_schema / dataclass_schema | Construct and manage class instances; dictionary schemas are simpler when no class is needed |
from pydantic_core import SchemaValidator, core_schema
schema = core_schema.list_schema(core_schema.int_schema(gt=0), min_length=1)
print(schema)
print(SchemaValidator(schema).validate_python(['2', 3])){'type': 'list', 'items_schema': {'type': 'int', 'gt': 0}, 'min_length': 1}
[2, 3]The schema requires a nonempty list of positive integers. SchemaValidator compiles those rules; validate_python() applies them and converts '2' to 2. In a model, put Field(gt=0) on the item type and Field(min_length=1) on the list field.
A core schema can contain Python functions and classes, so it is not generally JSON-serializable. JSON Schema describes an external data contract. core_schema.json_schema(inner) means “parse a JSON-encoded value stored inside a field”; it is not a JSON Schema generator. Invalid schema definitions raise SchemaError before input validation.
Official reference: pydantic-core: core_schema building blocks.
SchemaValidator: direct validation#
Compile one schema and reuse it to check Python objects or raw JSON.
from pydantic_core import SchemaValidator, ValidationError, core_schema
schema = core_schema.typed_dict_schema({
'sku': core_schema.typed_dict_field(core_schema.str_schema(pattern=r'^BK-[A-Z]+$')),
'quantity': core_schema.typed_dict_field(core_schema.int_schema(gt=0)),
'dry_run': core_schema.typed_dict_field(
core_schema.with_default_schema(core_schema.bool_schema(), default=False),
required=False
),
}, extra_behavior='forbid')
validator = SchemaValidator(schema)
print(validator.validate_python({'sku': 'BK-RUST', 'quantity': '2'}))
print(validator.validate_json('{"sku":"BK-RUST","quantity":2,"dry_run":true}'))
for raw in [
{'sku': 'BK-RUST', 'quantity': 0},
{'sku': 'BK-RUST'},
{'sku': 'BK-RUST', 'quantity': 2, 'admin': True},
]:
try:
validator.validate_python(raw)
except ValidationError as exc:
print([(e['loc'], e['type']) for e in exc.errors()])
try:
validator.validate_python({'sku': 'BK-RUST', 'quantity': '2'}, strict=True)
except ValidationError as exc:
print('strict:', exc.errors()[0]['type']){'sku': 'BK-RUST', 'quantity': 2, 'dry_run': False}
{'sku': 'BK-RUST', 'quantity': 2, 'dry_run': True}
[(('quantity',), 'greater_than')]
[(('quantity',), 'missing')]
[(('admin',), 'extra_forbidden')]
strict: int_typevalidate_python returns the value described by the schema: a dictionary here, not a BaseModel. validate_json parses and validates raw JSON in one operation. validate_strings handles string-valued inputs. Call-level strict, context, and other supported options apply to the same compiled validator. isinstance_python returns whether validation can succeed; it is not a static type check and may execute callbacks.
required=False allows a key to be absent. nullable_schema(...) allows explicit None. with_default_schema(...) can insert a default for missing input. Choose these separately; a nullable required field is still required.
typed_dict_schema returns dictionaries, not TypedDict classes. Each typed_dict_field defines a key’s rule; the dry-run field defaults to False. Reuse the compiled validator across calls.
Official reference: SchemaValidator: direct validation.
SchemaSerializer: direct Python and JSON output#
Serialize values using the same schema, after validation has established their shape.
from pydantic_core import SchemaValidator, SchemaSerializer, core_schema
schema = core_schema.typed_dict_schema({
'number': core_schema.typed_dict_field(core_schema.str_schema()),
'amount': core_schema.typed_dict_field(core_schema.decimal_schema(gt=0)),
'paid_at': core_schema.typed_dict_field(core_schema.datetime_schema(tz_constraint='aware')),
})
validator = SchemaValidator(schema)
serializer = SchemaSerializer(schema)
invoice = validator.validate_json(
'{"number":"INV-7","amount":"19.90","paid_at":"2026-09-30T06:00:00Z"}'
)
python_data = serializer.to_python(invoice)
json_data = serializer.to_python(invoice, mode='json')
json_bytes = serializer.to_json(invoice)
print(type(python_data['amount']).__name__, type(python_data['paid_at']).__name__)
print(json_data)
print(type(json_bytes).__name__, json_bytes.decode())
assert validator.validate_json(json_bytes) == invoice
print(serializer.to_python(invoice, exclude={'paid_at'}, mode='json'))Decimal datetime
{'number': 'INV-7', 'amount': '19.90', 'paid_at': '2026-09-30T06:00:00Z'}
bytes {"number":"INV-7","amount":"19.90","paid_at":"2026-09-30T06:00:00Z"}
{'number': 'INV-7', 'amount': '19.90'}to_python returns Python values; mode="json" makes them JSON-compatible. to_json returns bytes. A serializer may warn about mismatched types and does not enforce all input constraints. Validate first. Use warnings="error" to turn serialization warnings into errors when exporting data that must match its schema, while retaining validation as a separate step.
Official reference: SchemaSerializer: direct Python and JSON output.
Custom core validation, context, and serialization#
Compose native checks with the small amount of Python logic your domain requires.
Core offers before, after, plain, and wrap validation callbacks, with no_info_... variants or with_info_... variants that receive context and input mode. A serializer is attached under a node’s serialization key. Its behavior is separate from the validation callback.
from pydantic_core import SchemaValidator, SchemaSerializer, PydanticCustomError, ValidationError, core_schema
def clean(value):
return value.strip().upper() if isinstance(value, str) else value
def check_allowed(value, info):
blocked = (info.context or {}).get('blocked', set())
if value in blocked:
raise PydanticCustomError('invoice_blocked', 'Invoice is blocked by the import policy')
return value
schema = core_schema.with_info_after_validator_function(
check_allowed,
core_schema.no_info_before_validator_function(
clean, core_schema.str_schema(strict=True, pattern=r'^INV-[0-9]+$')
),
serialization=core_schema.plain_serializer_function_ser_schema(
lambda value: {'reference': value},
return_schema=core_schema.typed_dict_schema({
'reference': core_schema.typed_dict_field(core_schema.str_schema())
}),
when_used='json'
)
)
validator = SchemaValidator(schema)
serializer = SchemaSerializer(schema)
reference = validator.validate_python(' inv-42 ', context={'blocked': {'INV-99'}})
print(reference)
print(serializer.to_python(reference))
print(serializer.to_json(reference).decode())
for raw in ['bad', 'INV-99']:
try:
validator.validate_python(raw, context={'blocked': {'INV-99'}})
except ValidationError as exc:
print(exc.errors()[0]['type'])INV-42
INV-42
{"reference":"INV-42"}
string_pattern_mismatch
invoice_blockedThe validator accepts a string; the serializer emits an object for the partner’s export format. That output cannot be parsed by the same validator. Test round trips only for reversible formats. Prefer native constraints where possible to avoid Python callback overhead.
The before callback normalizes the reference, the native string rule checks its pattern, and the after callback checks the blocked set. The serializer emits a reference object. Its return_schema describes output and does not change accepted input.
Official reference: Custom core validation, context, and serialization.
Different Python and JSON input behavior#
Preserve rich local objects while accepting the appropriate wire representation.
from datetime import date
from pydantic_core import SchemaValidator, SchemaSerializer, core_schema, ValidationError
schema = core_schema.json_or_python_schema(
python_schema=core_schema.date_schema(strict=True),
json_schema=core_schema.date_schema(strict=True)
)
validator = SchemaValidator(schema)
serializer = SchemaSerializer(schema)
day = validator.validate_python(date(2026, 9, 30))
print(validator.validate_json('"2026-09-30"'))
print(serializer.to_json(day).decode())
try:
validator.validate_python('2026-09-30')
except ValidationError as exc:
print(exc.errors()[0]['type'])2026-09-30
"2026-09-30"
date_typeThe date schemas are both strict, but the original input mode still matters. validate_json stays in JSON mode through its branch; decoding with json.loads first and then calling validate_python is a different operation. Use json_or_python_schema for deliberately different contracts; use lax_or_strict_schema when the choice depends on strictness instead.
from pydantic_core import SchemaValidator, core_schema
def report_mode(value, info):
print(info.mode, value)
return value
validator = SchemaValidator(core_schema.with_info_after_validator_function(
report_mode, core_schema.int_schema()
))
assert validator.validate_python('7') == 7
assert validator.validate_json('7') == 7python 7
json 7A strict Python date must already be a date object; strict JSON accepts a date string. One strict date schema handles this built-in distinction. Use json_or_python_schema when a custom type needs different branch rules.
Official reference: Different Python and JSON input behavior.
Core tagged unions and recursive definitions#
Build reusable schema graphs when your integration constructs schemas dynamically.
from pydantic_core import SchemaValidator, core_schema, ValidationError
add_schema = core_schema.typed_dict_schema({
'action': core_schema.typed_dict_field(core_schema.literal_schema(['add'])),
'quantity': core_schema.typed_dict_field(core_schema.int_schema(gt=0)),
})
remove_schema = core_schema.typed_dict_schema({
'action': core_schema.typed_dict_field(core_schema.literal_schema(['remove'])),
'reason': core_schema.typed_dict_field(core_schema.str_schema(min_length=1)),
})
validator = SchemaValidator(core_schema.tagged_union_schema(
choices={'add': add_schema, 'remove': remove_schema}, discriminator='action'
))
print(validator.validate_json('{"action":"add","quantity":"3"}'))
try:
validator.validate_python({'action': 'move'})
except ValidationError as exc:
print(exc.errors()[0]['type']){'action': 'add', 'quantity': 3}
union_tag_invalidfrom pydantic_core import SchemaValidator, SchemaSerializer, core_schema
node = core_schema.typed_dict_schema({
'name': core_schema.typed_dict_field(core_schema.str_schema()),
'children': core_schema.typed_dict_field(
core_schema.with_default_schema(
core_schema.list_schema(core_schema.definition_reference_schema('folder')),
default_factory=list
), required=False
),
}, ref='folder')
schema = core_schema.definitions_schema(
core_schema.definition_reference_schema('folder'), definitions=[node]
)
validator = SchemaValidator(schema)
serializer = SchemaSerializer(schema)
tree = validator.validate_python({'name': 'papers', 'children': [{'name': '2026'}]})
print(serializer.to_json(tree).decode())
assert tree['children'][0]['children'] == []{"name":"papers","children":[{"name":"2026","children":[]}]}The ref name binds a schema node; it does not provide database IDs or serialize cyclic object graphs. Pydantic’s recursive BaseModel or a named type alias is usually easier to maintain. Choose core definitions when a schema-generation integration truly owns the graph.
The tagged union’s choices maps actions to schemas. For the tree, ref="folder" names a reusable schema and definition_reference_schema("folder") reuses it for children. definitions_schema supplies the definition and root.
Official reference: Core tagged unions and recursive definitions.
Function validation, dynamic models, and private attributes#
Apply validation beyond a conventional request class when the use case needs it.
from decimal import Decimal
from typing import Annotated
from pydantic import validate_call, Field, ValidationError
@validate_call(validate_return=True)
def line_total(price: Annotated[Decimal, Field(gt=0)],
quantity: Annotated[int, Field(gt=0)]) -> Decimal:
return price * quantity
print(line_total('9.90', '3'))
try:
line_total('9.90', 0)
except ValidationError as exc:
print(exc.errors()[0]['loc'], exc.errors()[0]['type'])29.70
(1,) greater_thanfrom pydantic import BaseModel, ConfigDict, Field, PrivateAttr, create_model
ImportShape = create_model(
'ImportShape', __config__=ConfigDict(extra='forbid'),
sku=(str, ...), quantity=(int, Field(gt=0))
)
print(ImportShape.model_validate({'sku': 'BK-RUST', 'quantity': '2'}).model_dump())
class Invoice(BaseModel):
number: str
_cache: dict = PrivateAttr(default_factory=dict)
invoice = Invoice(number='INV-7')
invoice._cache['pdf_ready'] = True
print(invoice.model_dump())
assert '_cache' not in invoice.model_json_schema()['properties']{'sku': 'BK-RUST', 'quantity': 2}
{'number': 'INV-7'}Private attributes are not fields: they are not validated or serialized. They are convenient instance state, not a security boundary. Dynamic field definitions should come from a trusted schema source; do not evaluate arbitrary code from user input. validate_call adds work to each call, so use it at useful boundaries rather than every internal helper.
Official reference: Function validation, dynamic models, and private attributes.
Partial JSON and the experimental missing sentinel#
Distinguish an incomplete stream from a partial update to a complete object.
A PATCH request is complete JSON with some fields omitted. Partial JSON parsing is different: it tries to recover values from a truncated stream. pydantic_core.from_json(..., allow_partial=True) parses the available data but does not validate its domain rules. TypeAdapter partial validation is explicitly experimental and can ignore errors near the end.
from pydantic import TypeAdapter
from pydantic_core import from_json
print(from_json('["INV-7", "INV-8", "INV-', allow_partial=True))
adapter = TypeAdapter(list[int])
print(adapter.validate_json('[1, 2, "unfinished', experimental_allow_partial=True))['INV-7', 'INV-8']
[1, 2]from pydantic import BaseModel
from pydantic.experimental.missing_sentinel import MISSING
class TimeoutPatch(BaseModel):
timeout_seconds: int | None | MISSING = MISSING
omitted = TimeoutPatch()
cleared = TimeoutPatch(timeout_seconds=None)
changed = TimeoutPatch(timeout_seconds=30)
print(omitted.model_dump(), cleared.model_dump(), changed.model_dump())
assert omitted.timeout_seconds is MISSING{} {'timeout_seconds': None} {'timeout_seconds': 30}Partial validation can accept some invalid complete inputs and drop a failing final item. It is useful for previews while streaming, not for committing a finished webhook or payment. MISSING is automatically excluded from dumps, but its API and type-checker support are experimental, and models holding it do not support pickling. Use the earlier model_fields_set recipe for a stable PATCH contract.
MISSING distinguishes omission from null: omitting the timeout excludes it from dumps, None clears it, and 30 sets it. Use model_fields_set for a stable alternative.
Official reference: Partial JSON and the experimental missing sentinel.
Performance and validation shortcuts#
Measure your actual boundary and reuse compiled schemas before changing abstractions.
| Choice | Trade-off |
|---|---|
| Reuse a model class, TypeAdapter, or core validator | Schema construction has a cost; avoid rebuilding in a loop |
Validate raw JSON with model_validate_json | Usually avoids a separate Python JSON-decoding step; custom before/wrap validators can change the trade-off |
Use concrete list/dict and tagged unions | Often simpler and faster than broad abstract containers or ambiguous unions |
| Use native constraints | Avoid unnecessary Rust-to-Python callbacks |
| TypedDict plus TypeAdapter | Useful dictionary-shaped boundary when model-instance conveniences are unnecessary |
| Direct core | More schema maintenance; no universal speed-up over Pydantic’s already compiled core |
model_construct | Skips validation and recursive construction; benchmark because ordinary validation can be equally fast or faster for simple models |
SkipValidation or Any | Reduces checking and weakens the contract |
from pydantic import BaseModel, Field
class Stock(BaseModel):
quantity: int = Field(ge=0)
stock = Stock(quantity=5)
copied = stock.model_copy(update={'quantity': 'broken'})
unchecked = Stock.model_construct(quantity=-1)
print(type(copied.quantity).__name__, unchecked.quantity)
assert copied.quantity == 'broken'
assert unchecked.quantity == -1
print('Neither shortcut checked the input')str -1
Neither shortcut checked the inputSeparate schema compilation, Python input validation, JSON parsing plus validation, and serialization. Reuse the same input corpus and include failures as well as successes. I/O, database access, and Python callbacks can dominate runtime. Do not drop checks to win a benchmark that no longer measures equivalent behavior.
Official reference: Performance and validation shortcuts.
Common mistakes and migration changes#
Recognize the shortcuts that change your contract or hide an invalid value.
| Mistake | Use instead / why |
|---|---|
Assume T | None means optional input | Give a default if omission is allowed |
Use exclude_none for PATCH changes | Use exclude_unset; preserve explicit null |
| Assume all default values are validated | Enable validate_default where needed |
| Trust existing model instances without checking policy | Use the model’s revalidate_instances when revalidation is required |
| Forget a validator return | Return the checked value; an after validator can otherwise store None |
| Make validators send emails or write data | Keep validation pure; perform actions after validation |
Use model_copy(update=...) for an untrusted patch | Merge allowed changes and validate the resulting model |
| Assume strict Python and JSON inputs behave identically | Test both paths for dates, UUIDs, and custom types |
Treat frozen as deep immutability | Use immutable inner values or protect mutation separately |
Use repr=False to hide a field from JSON | Use exclude=True or a public response model |
Expose raw errors() to users | Return selected paths, codes, and reviewed messages |
Access model_fields on an instance | Read ModelClass.model_fields |
| Assume field JSON Schema examples restrict input | Add actual constraints or validators |
| v1 spelling / pattern | v2 spelling / pattern |
|---|---|
dict() / json() | model_dump() / model_dump_json() |
parse_obj / parse_raw | model_validate / model_validate_json |
schema() | model_json_schema() |
construct / copy | model_construct / model_copy |
class Config | model_config = ConfigDict(...) |
@validator / @root_validator | @field_validator / @model_validator |
orm_mode / from_orm | from_attributes=True / model_validate |
__root__ | RootModel[T] |
BaseSettings from pydantic | Import from pydantic_settings |
json_encoders for new code | Field/model serializers or Annotated serializers |
V2 does not generally coerce numbers to strings or pairs-of-items into a dictionary the way v1 sometimes did. Use a deliberate parser or an explicit setting such as coerce_numbers_to_str for a real compatibility requirement. Model equality is not equality to a plain dictionary; compare a dump when that is your intent.
Official reference: Common mistakes and v1-to-v2 changes.
Tests for your own data boundaries#
Check accepted values, rejected values, and the data that actually leaves the application.
import json
import unittest
from datetime import date
from pydantic import BaseModel, ConfigDict, Field, SecretStr, TypeAdapter, ValidationError
from pydantic_core import SchemaValidator, SchemaSerializer, core_schema
class Request(BaseModel):
model_config = ConfigDict(extra='forbid')
quantity: int = Field(gt=0)
note: str | None = None
token: SecretStr = Field(exclude=True)
class BoundaryTests(unittest.TestCase):
def test_coercion_and_strictness(self):
self.assertEqual(Request(quantity='2', token='demo').quantity, 2)
with self.assertRaises(ValidationError):
Request.model_validate({'quantity': '2', 'token': 'demo'}, strict=True)
def test_omitted_and_null(self):
missing = Request(quantity=2, token='demo')
null = Request(quantity=2, token='demo', note=None)
self.assertNotIn('note', missing.model_dump(exclude_unset=True))
self.assertIn('note', null.model_dump(exclude_unset=True))
def test_errors_and_sensitive_output(self):
with self.assertRaises(ValidationError) as caught:
Request(quantity=0, token='demo')
self.assertEqual(caught.exception.errors()[0]['type'], 'greater_than')
output = Request(quantity=2, token='demo').model_dump_json()
self.assertNotIn('token', json.loads(output))
self.assertNotIn('demo', output)
def test_strict_date_input_modes(self):
adapter = TypeAdapter(date)
self.assertEqual(adapter.validate_json('"2026-09-30"', strict=True), date(2026, 9, 30))
with self.assertRaises(ValidationError):
adapter.validate_python('2026-09-30', strict=True)
def test_direct_core_round_trip(self):
schema = core_schema.list_schema(core_schema.int_schema(gt=0))
validator, serializer = SchemaValidator(schema), SchemaSerializer(schema)
value = validator.validate_json('[1,2,3]')
self.assertEqual(validator.validate_json(serializer.to_json(value)), value)
with self.assertRaises(ValidationError):
validator.validate_python([1, 0])
suite = unittest.defaultTestLoader.loadTestsFromTestCase(BoundaryTests)
result = unittest.TextTestRunner(verbosity=0).run(suite)
if not result.wasSuccessful():
raise SystemExit(1)
print(f'{result.testsRun} boundary tests passed')5 boundary tests passed| Case | What to assert |
|---|---|
| Valid and invalid inputs | Stored Python types and stable error codes |
| Strict versus coercive | Python and JSON paths separately |
| Missing versus null | model_fields_set and actual patch output |
| Nested errors and unions | Paths include the correct field/index and selected tag |
| Validators and serializers | Rules run in the right order, preserve intended type, and shape the output |
| Context | Trusted policy affects validation; missing context fails safely |
| Sensitive values | Absent from public dumps and error responses |
| JSON Schema | Input/output properties match the actual wire representation |
| Core schemas and upgrades | Compilation, valid/invalid input, serialization, and reversible round trips |
Official reference: Tests for your own data boundaries.
Quick reference and official documentation#
Choose a validation tool by the shape of the input and the output you need.
| Need | Choose |
|---|---|
| A normal API record | BaseModel |
| A list, union, or standalone domain type | TypeAdapter |
| A model wrapping a top-level scalar or array | RootModel |
| A validated dataclass | pydantic.dataclasses.dataclass |
| Simple reusable constraints | Annotated[T, Field(...)] |
| Reusable custom logic | BeforeValidator / AfterValidator / WrapValidator |
| A custom runtime type | A core schema hook plus serialization and JSON Schema behavior |
| A custom schema engine | Direct core_schema + SchemaValidator + SchemaSerializer |
| Environment and secrets sources | pydantic-settings |
- Pydantic documentation and installed-version policy
- Models, validation methods, generics, and ORM attributes
- Fields, constraints, defaults, and computed fields
- Strict mode and the conversion table
- Unions and nested discriminators
- Validators, ordering, and context
- Serialization, exclusions, and subclass behavior
- Custom types and schema hooks
- JSON Schema generation
- pydantic-core validator and serializer API
- core_schema builders and callback signatures
- pydantic-settings sources and settings configuration
- email-validator normalization and deliverability
- Country and coordinate types
- Performance guidance
- Experimental partial validation and MISSING
- V1 migration guide
Features marked “2.13” require that release or newer. Experimental APIs may change between releases. Check the installed version with python -c "import pydantic; print(pydantic.__version__)".
Help improve CodeRef
Allow Google Analytics to measure page visits and use of the guides? It uses cookies after you accept. Your choice won’t affect access to the site. Privacy notice