Please ask questions as I go along ... I've written this in a hurry so if you're confused, you're probably not alone
Everything! (and hopefully nothing for you):
all validation is offloaded to another library - pydantic-core which I'm building now
"You can't make a Tomlette without breaking a few Gregs!" (Succession S2E9) - best TV pun ever?
I'm going to have to upset some people to get V2 out:
class MyModel(BaseModel):
appointment_time: datetime | None
@validator('appointment_time', mode='wrap')
def validate_appointment_time(cls, v, handler):
if v == 'now':
return datetime.now()
try:
return handler(v)
except ValidationError:
# we don't want to fail, so just use None
return None
(Implemented, but not with this nice syntactic sugar)
AKA "The onion" - like middleware
class StrictModel(BaseModel, strict=True):
a_string: str
an_int: int
set_of_ints: Set[int]
class LaxModel(BaseModel):
lax_string: str
strict_string1: str = Field(..., strict=True)
strict_string2: StrictStr
(Implemented, but not with this nice syntactic sugar)
(Does not apply when "downcasting" from JSON)
class MyModel(BaseModel):
int_or_bool: int | bool
bool_or_int: bool | int
print(MyModel(int_or_bool=1)) #> 1, 1
print(MyModel(int_or_bool=True)) #> True, True
print(MyModel(int_or_bool='1')) #> 1, True :-( ?
(Implemented, but not with this nice syntactic sugar)
(Using strict mode)
(Also works with models and model instances)
class MyModel1(BaseModel):
none_allowed_required: str | None
none_allowed_not_required: str | None = None
from typing_extentions import Required, NotRequired
class MyModel2(BaseModel): # ... maybe, do you want this?
none_allowed_required: Required[str]
none_allowed_not_required: NotRequired[str]
(Implemented, but not with this nice syntactic sugar)
(I'm no longer scared of the word "optional")
from pydantic import validate
validate(List[int], [1, 2, '3']) #> [1, 2, 3]
validate(List[int], [1, 2, 3], strict=True) #> [1, 2, 3]
validate(List[int], [1, 2, '3'], strict=True)
#> raises ValidationError
(Implemented, but not with this nice syntactic sugar)
No intermediate model required (unlike parse_obj in V1)
class MyModel(BaseModel):
name: str
age: int
friends: List[int]
settings: Dict[str, float]
MyModel.validate_json('{...}')
(Implemented, but not with this nice syntactic sugar)
No json.loads - just rust JSON parsing straight into validation
Benchmark | Speed up |
---|---|
Simple model (str, int, List[int], Dict[str, float]) | 15.97x |
Simple model - JSON | 11.56x |
A bool (single value) | 3.46x |
Recursive model, 50 deep | 3.99x |
list of typed dicts, length 100 | 12.14x |
list of ints, length 1000 | 25.49x |
from pydantic_core import SchemaValidator
schema_validator = SchemaValidator({'type': 'bool'})
print(repr(schema_validator))
(This code actually runs now!)
Let's start simple
SchemaValidator(name="bool", validator=BoolValidator)
print(schema_validator.validate_python(True)) -> True
print(schema_validator.validate_python(1)) -> True
print(schema_validator.validate_json('true')) -> True
from pydantic_core import SchemaValidator
# Equivalent to: Dict[str, Optional[int]]
schema_validator = SchemaValidator({
'type': 'dict',
'keys': {'type': 'str'},
'values': {'type': 'optional', 'schema': {'type': 'int'}}
})
Let's get a bit more complicated
SchemaValidator(name="dict", validator=DictValidator {
strict: false,
key_validator: Some(StrValidator),
value_validator: Some(
OptionalValidator {validator: IntValidator},
),
min_items: None,
max_items: None,
try_instance_as_dict: false,
})
class MyModel(BaseModel):
name: str
age: int | None = 42
settings: dict[str, float]
friends: list[int | str]
And finally...
(you don't need to read all this)
SchemaValidator(name="MyCoreModel",
validator=ModelClassValidator {
strict: false,
class: Py(0x12fe7e7c0), (MyCoreModel)
new_method: Py(0x00101054130), (MyCoreModel.__new__)
validator: ModelValidator {
name: "Model",
fields: [
ModelField {
name: "name",
default: None,
validator: StrValidator,
},
ModelField {
name: "age",
default: 42,
validator: OptionalValidator {
validator: IntValidator
},
},
ModelField {
name: "settings",
default: None,
validator: DictValidator { ... },
},
ModelField {
name: "friends",
default: None,
validator: ListValidator {
strict: false,
item_validator: Some(
UnionValidator {
choices: [
IntValidator,
StrValidator,
],
},
),
min_items: None,
max_items: None,
},
},
],
extra_behavior: Ignore,
extra_validator: None,
},
})
Many other people have said all this, there are many (much better) talks about it.
But for completeness, the good:
The bad:
The ugly:
#[derive(Debug, Clone)]
pub struct OptionalValidator {
validator: Box<dyn Validator>,
}
impl OptionalValidator {
pub const EXPECTED_TYPE: &'static str = "optional";
}
impl Validator for OptionalValidator {
fn build(schema: &PyDict, config: Option<&PyDict>) -> PyResult<Box<dyn Validator>> {
let schema: &PyAny = schema.get_as_req("schema")?;
Ok(Box::new(Self {
validator: build_validator(schema, config)?.0,
}))
}
fn validate<'s, 'data>(
&'s self,
py: Python<'data>,
input: &'data dyn Input,
extra: &Extra,
) -> ValResult<'data, PyObject> {
match input.is_none() {
true => Ok(py.None()),
false => self.validator.validate(py, input, extra),
}
}
fn validate_strict<'s, 'data>(
&'s self,
py: Python<'data>,
input: &'data dyn Input,
extra: &Extra,
) -> ValResult<'data, PyObject> {
match input.is_none() {
true => Ok(py.None()),
false => self.validator.validate_strict(py, input, extra),
}
}
fn set_ref(&mut self, name: &str, validator_arc: &ValidatorArc) -> PyResult<()> {
self.validator.set_ref(name, validator_arc)
}
validator_boilerplate!(Self::EXPECTED_TYPE);
}
mod optional;
...
lots of code...
...
validator_match!(
type_,
dict,
config,
... all the other validators
// unions
self::union::UnionValidator,
self::optional::OptionalValidator,
...
)
Checkout: github.com/samuelcolvin/pydantic-core
And: github.com/samuelcolvin/pydantic
Follow me on twitter: @samuel_colvin