Skip to content

Commit c1e0111

Browse files
Add JsonOrPython validator and serializer (#598)
Co-authored-by: Samuel Colvin <[email protected]>
1 parent 027e679 commit c1e0111

28 files changed

+421
-293
lines changed

pydantic_core/_pydantic_core.pyi

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,6 @@ class SchemaValidator:
5353
context: Any = None,
5454
self_instance: 'Any | None' = None,
5555
) -> Any: ...
56-
def isinstance_json(
57-
self,
58-
input: 'str | bytes | bytearray',
59-
*,
60-
strict: 'bool | None' = None,
61-
context: Any = None,
62-
self_instance: 'Any | None' = None,
63-
) -> bool: ...
6456
def validate_assignment(
6557
self, obj: Any, field: str, input: Any, *, strict: 'bool | None' = None, context: Any = None
6658
) -> 'dict[str, Any]': ...

pydantic_core/core_schema.py

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ def config(self) -> CoreConfig | None:
124124
"""The CoreConfig that applies to this validation."""
125125
...
126126

127+
@property
128+
def mode(self) -> Literal['python', 'json']:
129+
"""The type of input data we are currently validating"""
130+
...
131+
127132

128133
class FieldValidationInfo(ValidationInfo, Protocol):
129134
"""
@@ -1050,8 +1055,6 @@ class IsInstanceSchema(TypedDict, total=False):
10501055
type: Required[Literal['is-instance']]
10511056
cls: Required[Any]
10521057
cls_repr: str
1053-
json_types: Set[JsonType]
1054-
json_function: Callable[[Any], Any]
10551058
ref: str
10561059
metadata: Any
10571060
serialization: SerSchema
@@ -1060,8 +1063,6 @@ class IsInstanceSchema(TypedDict, total=False):
10601063
def is_instance_schema(
10611064
cls: Any,
10621065
*,
1063-
json_types: Set[JsonType] | None = None,
1064-
json_function: Callable[[Any], Any] | None = None,
10651066
cls_repr: str | None = None,
10661067
ref: str | None = None,
10671068
metadata: Any = None,
@@ -1083,23 +1084,13 @@ class A:
10831084
10841085
Args:
10851086
cls: The value must be an instance of this class
1086-
json_types: When parsing JSON directly, the value must be one of these json types
1087-
json_function: When parsing JSON directly, If provided, the JSON value is passed to this
1088-
function and the return value used as the output value
10891087
cls_repr: If provided this string is used in the validator name instead of `repr(cls)`
10901088
ref: optional unique identifier of the schema, used to reference the schema in other places
10911089
metadata: Any other information you want to include with the schema, not used by pydantic-core
10921090
serialization: Custom serialization schema
10931091
"""
10941092
return dict_not_none(
1095-
type='is-instance',
1096-
cls=cls,
1097-
json_types=json_types,
1098-
json_function=json_function,
1099-
cls_repr=cls_repr,
1100-
ref=ref,
1101-
metadata=metadata,
1102-
serialization=serialization,
1093+
type='is-instance', cls=cls, cls_repr=cls_repr, ref=ref, metadata=metadata, serialization=serialization
11031094
)
11041095

11051096

@@ -2604,6 +2595,63 @@ def fn(v: str, info: core_schema.ValidationInfo) -> str:
26042595
)
26052596

26062597

2598+
class JsonOrPythonSchema(TypedDict, total=False):
2599+
type: Required[Literal['json-or-python']]
2600+
json_schema: Required[CoreSchema]
2601+
python_schema: Required[CoreSchema]
2602+
ref: str
2603+
metadata: Any
2604+
serialization: SerSchema
2605+
2606+
2607+
def json_or_python_schema(
2608+
json_schema: CoreSchema,
2609+
python_schema: CoreSchema,
2610+
*,
2611+
ref: str | None = None,
2612+
metadata: Any = None,
2613+
serialization: SerSchema | None = None,
2614+
) -> JsonOrPythonSchema:
2615+
"""
2616+
Returns a schema that uses the Json or Python schema depending on the input:
2617+
2618+
```py
2619+
from pydantic_core import SchemaValidator, ValidationError, core_schema
2620+
2621+
v = SchemaValidator(
2622+
core_schema.json_or_python_schema(
2623+
json_schema=core_schema.int_schema(),
2624+
python_schema=core_schema.int_schema(strict=True),
2625+
)
2626+
)
2627+
2628+
assert v.validate_json('"123"') == 123
2629+
2630+
try:
2631+
v.validate_python('123')
2632+
except ValidationError:
2633+
pass
2634+
else:
2635+
raise AssertionError('Validation should have failed')
2636+
```
2637+
2638+
Args:
2639+
json_schema: The schema to use for Json inputs
2640+
python_schema: The schema to use for Python inputs
2641+
ref: optional unique identifier of the schema, used to reference the schema in other places
2642+
metadata: Any other information you want to include with the schema, not used by pydantic-core
2643+
serialization: Custom serialization schema
2644+
"""
2645+
return dict_not_none(
2646+
type='json-or-python',
2647+
json_schema=json_schema,
2648+
python_schema=python_schema,
2649+
ref=ref,
2650+
metadata=metadata,
2651+
serialization=serialization,
2652+
)
2653+
2654+
26072655
class TypedDictField(TypedDict, total=False):
26082656
type: Required[Literal['typed-dict-field']]
26092657
schema: Required[CoreSchema]
@@ -3612,6 +3660,7 @@ def definition_reference_schema(
36123660
TaggedUnionSchema,
36133661
ChainSchema,
36143662
LaxOrStrictSchema,
3663+
JsonOrPythonSchema,
36153664
TypedDictSchema,
36163665
ModelFieldsSchema,
36173666
ModelSchema,
@@ -3664,6 +3713,7 @@ def definition_reference_schema(
36643713
'tagged-union',
36653714
'chain',
36663715
'lax-or-strict',
3716+
'json-or-python',
36673717
'typed-dict',
36683718
'model-fields',
36693719
'model',

src/input/input_abstract.rs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::fmt;
22

3-
use pyo3::prelude::*;
43
use pyo3::types::{PyDict, PyString, PyType};
4+
use pyo3::{intern, prelude::*};
55

66
use crate::errors::{InputValue, LocItem, ValResult};
77
use crate::{PyMultiHostUrl, PyUrl};
@@ -10,15 +10,18 @@ use super::datetime::{EitherDate, EitherDateTime, EitherTime, EitherTimedelta};
1010
use super::return_enums::{EitherBytes, EitherString};
1111
use super::{GenericArguments, GenericCollection, GenericIterator, GenericMapping, JsonInput};
1212

13+
#[derive(Debug, Clone, Copy)]
1314
pub enum InputType {
1415
Python,
1516
Json,
16-
String,
1717
}
1818

19-
impl InputType {
20-
pub fn is_json(&self) -> bool {
21-
matches!(self, Self::Json)
19+
impl IntoPy<PyObject> for InputType {
20+
fn into_py(self, py: Python<'_>) -> PyObject {
21+
match self {
22+
Self::Json => intern!(py, "json").into(),
23+
Self::Python => intern!(py, "python").into(),
24+
}
2225
}
2326
}
2427

@@ -27,8 +30,6 @@ impl InputType {
2730
/// * `strict_*` & `lax_*` if they have different behavior
2831
/// * or, `validate_*` and `strict_*` to just call `validate_*` if the behavior for strict and lax is the same
2932
pub trait Input<'a>: fmt::Debug + ToPyObject {
30-
fn get_type(&self) -> &'static InputType;
31-
3233
fn as_loc_item(&self) -> LocItem;
3334

3435
fn as_error_value(&'a self) -> InputValue<'a>;
@@ -44,9 +45,6 @@ pub trait Input<'a>: fmt::Debug + ToPyObject {
4445
None
4546
}
4647

47-
// input_ prefix to differentiate from the function on PyAny
48-
fn input_is_instance(&self, class: &PyAny, json_mask: u8) -> PyResult<bool>;
49-
5048
fn is_exact_instance(&self, _class: &PyType) -> bool {
5149
false
5250
}

src/input/input_json.rs

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,14 @@ use super::datetime::{
77
bytes_as_date, bytes_as_datetime, bytes_as_time, bytes_as_timedelta, float_as_datetime, float_as_duration,
88
float_as_time, int_as_datetime, int_as_duration, int_as_time, EitherDate, EitherDateTime, EitherTime,
99
};
10-
use super::input_abstract::InputType;
1110
use super::parse_json::JsonArray;
1211
use super::shared::{float_as_int, int_as_bool, map_json_err, str_as_bool, str_as_int};
1312
use super::{
1413
EitherBytes, EitherString, EitherTimedelta, GenericArguments, GenericCollection, GenericIterator, GenericMapping,
15-
Input, JsonArgs, JsonInput, JsonType,
14+
Input, JsonArgs, JsonInput,
1615
};
1716

1817
impl<'a> Input<'a> for JsonInput {
19-
fn get_type(&self) -> &'static InputType {
20-
&InputType::Json
21-
}
22-
2318
/// This is required by since JSON object keys are always strings, I don't think it can be called
2419
#[cfg_attr(has_no_coverage, no_coverage)]
2520
fn as_loc_item(&self) -> LocItem {
@@ -38,23 +33,6 @@ impl<'a> Input<'a> for JsonInput {
3833
matches!(self, JsonInput::Null)
3934
}
4035

41-
fn input_is_instance(&self, _class: &PyAny, json_mask: u8) -> PyResult<bool> {
42-
if json_mask == 0 {
43-
Ok(false)
44-
} else {
45-
let json_type: JsonType = match self {
46-
JsonInput::Null => JsonType::Null,
47-
JsonInput::Bool(_) => JsonType::Bool,
48-
JsonInput::Int(_) => JsonType::Int,
49-
JsonInput::Float(_) => JsonType::Float,
50-
JsonInput::String(_) => JsonType::String,
51-
JsonInput::Array(_) => JsonType::Array,
52-
JsonInput::Object(_) => JsonType::Object,
53-
};
54-
Ok(json_type.matches(json_mask))
55-
}
56-
}
57-
5836
fn as_kwargs(&'a self, py: Python<'a>) -> Option<&'a PyDict> {
5937
match self {
6038
JsonInput::Object(object) => {
@@ -330,10 +308,6 @@ impl<'a> Input<'a> for JsonInput {
330308

331309
/// Required for Dict keys so the string can behave like an Input
332310
impl<'a> Input<'a> for String {
333-
fn get_type(&self) -> &'static InputType {
334-
&InputType::String
335-
}
336-
337311
fn as_loc_item(&self) -> LocItem {
338312
self.to_string().into()
339313
}
@@ -347,14 +321,6 @@ impl<'a> Input<'a> for String {
347321
false
348322
}
349323

350-
fn input_is_instance(&self, _class: &PyAny, json_mask: u8) -> PyResult<bool> {
351-
if json_mask == 0 {
352-
Ok(false)
353-
} else {
354-
Ok(JsonType::String.matches(json_mask))
355-
}
356-
}
357-
358324
fn as_kwargs(&'a self, _py: Python<'a>) -> Option<&'a PyDict> {
359325
None
360326
}

src/input/input_python.rs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,10 @@ use super::datetime::{
2020
float_as_duration, float_as_time, int_as_datetime, int_as_duration, int_as_time, EitherDate, EitherDateTime,
2121
EitherTime,
2222
};
23-
use super::input_abstract::InputType;
2423
use super::shared::{float_as_int, int_as_bool, map_json_err, str_as_bool, str_as_int};
2524
use super::{
26-
py_error_on_minusone, py_string_str, EitherBytes, EitherString, EitherTimedelta, GenericArguments,
27-
GenericCollection, GenericIterator, GenericMapping, Input, JsonInput, PyArgs,
25+
py_string_str, EitherBytes, EitherString, EitherTimedelta, GenericArguments, GenericCollection, GenericIterator,
26+
GenericMapping, Input, JsonInput, PyArgs,
2827
};
2928

3029
/// Extract generators and deques into a `GenericCollection`
@@ -72,10 +71,6 @@ macro_rules! extract_dict_iter {
7271
}
7372

7473
impl<'a> Input<'a> for PyAny {
75-
fn get_type(&self) -> &'static InputType {
76-
&InputType::Python
77-
}
78-
7974
fn as_loc_item(&self) -> LocItem {
8075
if let Ok(py_str) = self.downcast::<PyString>() {
8176
py_str.to_string_lossy().as_ref().into()
@@ -102,14 +97,6 @@ impl<'a> Input<'a> for PyAny {
10297
Some(self.getattr(name))
10398
}
10499

105-
fn input_is_instance(&self, class: &PyAny, _json_mask: u8) -> PyResult<bool> {
106-
// See PyO3/pyo3#2694 - we can't use `is_instance` here since it requires PyType,
107-
// and some check objects are not types, this logic is lifted from `is_instance` in PyO3
108-
let result = unsafe { ffi::PyObject_IsInstance(self.as_ptr(), class.as_ptr()) };
109-
py_error_on_minusone(self.py(), result)?;
110-
Ok(result == 1)
111-
}
112-
113100
fn is_exact_instance(&self, class: &PyType) -> bool {
114101
self.get_type().is(class)
115102
}

src/input/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ pub(crate) use datetime::{
1414
pydate_as_date, pydatetime_as_datetime, pytime_as_time, pytimedelta_as_duration, EitherDate, EitherDateTime,
1515
EitherTime, EitherTimedelta,
1616
};
17-
pub(crate) use input_abstract::Input;
18-
pub(crate) use parse_json::{JsonInput, JsonObject, JsonType};
17+
pub(crate) use input_abstract::{Input, InputType};
18+
pub(crate) use parse_json::{JsonInput, JsonObject};
1919
pub(crate) use return_enums::{
2020
py_string_str, AttributesGenericIterator, DictGenericIterator, EitherBytes, EitherString, GenericArguments,
2121
GenericCollection, GenericIterator, GenericMapping, JsonArgs, JsonObjectGenericIterator, MappingGenericIterator,

src/input/parse_json.rs

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,11 @@
11
use std::fmt;
22

33
use pyo3::prelude::*;
4-
use pyo3::types::{PyDict, PyList, PySet};
4+
use pyo3::types::{PyDict, PyList};
55
use serde::de::{Deserialize, DeserializeSeed, Error as SerdeError, MapAccess, SeqAccess, Visitor};
66

77
use crate::lazy_index_map::LazyIndexMap;
88

9-
use crate::build_tools::py_err;
10-
11-
#[derive(Copy, Clone, Debug)]
12-
pub enum JsonType {
13-
Null = 0b10000000,
14-
Bool = 0b01000000,
15-
Int = 0b00100000,
16-
Float = 0b00010000,
17-
String = 0b00001000,
18-
Array = 0b00000100,
19-
Object = 0b00000010,
20-
}
21-
22-
impl JsonType {
23-
pub fn combine(set: &PySet) -> PyResult<u8> {
24-
set.iter().map(Self::try_from).try_fold(0u8, |a, b| Ok(a | b? as u8))
25-
}
26-
27-
pub fn matches(&self, mask: u8) -> bool {
28-
*self as u8 & mask > 0
29-
}
30-
}
31-
32-
impl TryFrom<&PyAny> for JsonType {
33-
type Error = PyErr;
34-
35-
fn try_from(value: &PyAny) -> PyResult<Self> {
36-
let s: &str = value.extract()?;
37-
match s {
38-
"null" => Ok(Self::Null),
39-
"bool" => Ok(Self::Bool),
40-
"int" => Ok(Self::Int),
41-
"float" => Ok(Self::Float),
42-
"str" => Ok(Self::String),
43-
"list" => Ok(Self::Array),
44-
"dict" => Ok(Self::Object),
45-
_ => py_err!("Invalid json type: {}", s),
46-
}
47-
}
48-
}
49-
509
/// similar to serde `Value` but with int and float split
5110
#[derive(Clone, Debug)]
5211
pub enum JsonInput {

src/serializers/shared.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ combined_serializer! {
130130
ToString: super::type_serializers::format::ToStringSerializer;
131131
WithDefault: super::type_serializers::with_default::WithDefaultSerializer;
132132
Json: super::type_serializers::json::JsonSerializer;
133+
JsonOrPython: super::type_serializers::json_or_python::JsonOrPythonSerializer;
133134
Union: super::type_serializers::union::UnionSerializer;
134135
Literal: super::type_serializers::literal::LiteralSerializer;
135136
Recursive: super::type_serializers::definitions::DefinitionRefSerializer;

0 commit comments

Comments
 (0)