Skip to content

Commit 6e7fc01

Browse files
committed
Switch to val_json_bytes config key
1 parent c1c84e3 commit 6e7fc01

File tree

13 files changed

+68
-32
lines changed

13 files changed

+68
-32
lines changed

python/pydantic_core/core_schema.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class CoreConfig(TypedDict, total=False):
7070
ser_json_bytes: The serialization option for `bytes` values. Default is 'utf8'.
7171
ser_json_inf_nan: The serialization option for infinity and NaN values
7272
in float fields. Default is 'null'.
73+
val_json_bytes: The validation option for `bytes` values, complementing ser_json_bytes. Default is 'utf8'.
7374
hide_input_in_errors: Whether to hide input data from `ValidationError` representation.
7475
validation_error_cause: Whether to add user-python excs to the __cause__ of a ValidationError.
7576
Requires exceptiongroup backport pre Python 3.11.
@@ -107,6 +108,7 @@ class CoreConfig(TypedDict, total=False):
107108
ser_json_timedelta: Literal['iso8601', 'float'] # default: 'iso8601'
108109
ser_json_bytes: Literal['utf8', 'base64', 'hex'] # default: 'utf8'
109110
ser_json_inf_nan: Literal['null', 'constants', 'strings'] # default: 'null'
111+
val_json_bytes: Literal['utf8', 'base64', 'hex'] # default: 'utf8'
110112
# used to hide input data from ValidationError repr
111113
hide_input_in_errors: bool
112114
validation_error_cause: bool # default: False

src/input/input_abstract.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use pyo3::{intern, prelude::*};
66

77
use crate::errors::{ErrorTypeDefaults, InputValue, LocItem, ValError, ValResult};
88
use crate::lookup_key::{LookupKey, LookupPath};
9-
use crate::serializers::config::BytesMode;
109
use crate::tools::py_err;
10+
use crate::validators::config::ValBytesMode;
1111

1212
use super::datetime::{EitherDate, EitherDateTime, EitherTime, EitherTimedelta};
1313
use super::return_enums::{EitherBytes, EitherInt, EitherString};
@@ -72,7 +72,7 @@ pub trait Input<'py>: fmt::Debug + ToPyObject {
7272

7373
fn validate_str(&self, strict: bool, coerce_numbers_to_str: bool) -> ValMatch<EitherString<'_>>;
7474

75-
fn validate_bytes<'a>(&'a self, strict: bool, mode: BytesMode) -> ValMatch<EitherBytes<'a, 'py>>;
75+
fn validate_bytes<'a>(&'a self, strict: bool, mode: ValBytesMode) -> ValMatch<EitherBytes<'a, 'py>>;
7676

7777
fn validate_bool(&self, strict: bool) -> ValMatch<bool>;
7878

src/input/input_json.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use strum::EnumMessage;
99

1010
use crate::errors::{ErrorType, ErrorTypeDefaults, InputValue, LocItem, ValError, ValResult};
1111
use crate::lookup_key::{LookupKey, LookupPath};
12-
use crate::serializers::config::BytesMode;
12+
use crate::validators::config::ValBytesMode;
1313
use crate::validators::decimal::create_decimal;
1414

1515
use super::datetime::{
@@ -110,7 +110,7 @@ impl<'py, 'data> Input<'py> for JsonValue<'data> {
110110
fn validate_bytes<'a>(
111111
&'a self,
112112
_strict: bool,
113-
mode: BytesMode,
113+
mode: ValBytesMode,
114114
) -> ValResult<ValidationMatch<EitherBytes<'a, 'py>>> {
115115
match self {
116116
JsonValue::Str(s) => match mode.deserialize_string(s) {
@@ -353,7 +353,7 @@ impl<'py> Input<'py> for str {
353353
fn validate_bytes<'a>(
354354
&'a self,
355355
_strict: bool,
356-
mode: BytesMode,
356+
mode: ValBytesMode,
357357
) -> ValResult<ValidationMatch<EitherBytes<'a, 'py>>> {
358358
match mode.deserialize_string(self) {
359359
Ok(b) => Ok(ValidationMatch::strict(b)),

src/input/input_python.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ use pyo3::PyTypeCheck;
1414
use speedate::MicrosecondsPrecisionOverflowBehavior;
1515

1616
use crate::errors::{ErrorType, ErrorTypeDefaults, InputValue, LocItem, ValError, ValResult};
17-
use crate::serializers::config::BytesMode;
1817
use crate::tools::{extract_i64, safe_repr};
18+
use crate::validators::config::ValBytesMode;
1919
use crate::validators::decimal::{create_decimal, get_decimal_type};
2020
use crate::validators::Exactness;
2121
use crate::ArgsKwargs;
@@ -175,7 +175,11 @@ impl<'py> Input<'py> for Bound<'py, PyAny> {
175175
Err(ValError::new(ErrorTypeDefaults::StringType, self))
176176
}
177177

178-
fn validate_bytes<'a>(&'a self, strict: bool, mode: BytesMode) -> ValResult<ValidationMatch<EitherBytes<'a, 'py>>> {
178+
fn validate_bytes<'a>(
179+
&'a self,
180+
strict: bool,
181+
mode: ValBytesMode,
182+
) -> ValResult<ValidationMatch<EitherBytes<'a, 'py>>> {
179183
if let Ok(py_bytes) = self.downcast_exact::<PyBytes>() {
180184
return Ok(ValidationMatch::exact(py_bytes.into()));
181185
} else if let Ok(py_bytes) = self.downcast::<PyBytes>() {

src/input/input_string.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use speedate::MicrosecondsPrecisionOverflowBehavior;
66
use crate::errors::{ErrorTypeDefaults, InputValue, LocItem, ValError, ValResult};
77
use crate::input::py_string_str;
88
use crate::lookup_key::{LookupKey, LookupPath};
9-
use crate::serializers::config::BytesMode;
109
use crate::tools::safe_repr;
10+
use crate::validators::config::ValBytesMode;
1111
use crate::validators::decimal::create_decimal;
1212

1313
use super::datetime::{
@@ -109,7 +109,7 @@ impl<'py> Input<'py> for StringMapping<'py> {
109109
fn validate_bytes<'a>(
110110
&'a self,
111111
_strict: bool,
112-
mode: BytesMode,
112+
mode: ValBytesMode,
113113
) -> ValResult<ValidationMatch<EitherBytes<'a, 'py>>> {
114114
match self {
115115
Self::String(s) => py_string_str(s).and_then(|b| match mode.deserialize_string(b) {

src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use jiter::{map_json_error, PartialMode, PythonParse, StringCacheMode};
88
use pyo3::exceptions::PyTypeError;
99
use pyo3::{prelude::*, sync::GILOnceCell};
1010
use serializers::config::BytesMode;
11+
use validators::config::ValBytesMode;
1112

1213
// parse this first to get access to the contained macro
1314
#[macro_use]
@@ -56,7 +57,7 @@ pub fn from_json<'py>(
5657
allow_partial: bool,
5758
) -> PyResult<Bound<'py, PyAny>> {
5859
let v_match = data
59-
.validate_bytes(false, BytesMode::Utf8)
60+
.validate_bytes(false, ValBytesMode { ser: BytesMode::Utf8 })
6061
.map_err(|_| PyTypeError::new_err("Expected bytes, bytearray or str"))?;
6162
let json_either_bytes = v_match.into_inner();
6263
let json_bytes = json_either_bytes.as_slice();

src/serializers/config.rs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@ use std::borrow::Cow;
22
use std::str::{from_utf8, FromStr, Utf8Error};
33

44
use base64::Engine;
5-
use pyo3::exceptions::PyValueError;
65
use pyo3::intern;
76
use pyo3::prelude::*;
87
use pyo3::types::{PyDelta, PyDict, PyString};
98

109
use serde::ser::Error;
1110

1211
use crate::build_tools::py_schema_err;
13-
use crate::input::{EitherBytes, EitherTimedelta};
12+
use crate::input::EitherTimedelta;
1413
use crate::tools::SchemaDict;
1514

1615
use super::errors::py_err_se_err;
@@ -189,17 +188,6 @@ impl BytesMode {
189188
}
190189
}
191190
}
192-
193-
pub fn deserialize_string<'a, 'py>(&self, s: &'a str) -> PyResult<EitherBytes<'a, 'py>> {
194-
match self {
195-
Self::Utf8 => Ok(EitherBytes::Cow(Cow::Borrowed(s.as_bytes()))),
196-
Self::Base64 => match base64::engine::general_purpose::URL_SAFE.decode(s) {
197-
Ok(bytes) => Ok(EitherBytes::from(bytes)),
198-
Err(err) => Err(PyValueError::new_err(format!("Base64 decode error: {err}"))),
199-
},
200-
Self::Hex => Err(PyValueError::new_err("Hex deserialization is not supported")),
201-
}
202-
}
203191
}
204192

205193
pub fn utf8_py_error(py: Python, err: Utf8Error, data: &[u8]) -> PyErr {

src/validators/bytes.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ use crate::build_tools::is_strict;
66
use crate::errors::{ErrorType, ValError, ValResult};
77
use crate::input::Input;
88

9-
use crate::serializers::config::{BytesMode, FromConfig};
109
use crate::tools::SchemaDict;
1110

11+
use super::config::ValBytesMode;
1212
use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator};
1313

1414
#[derive(Debug, Clone)]
1515
pub struct BytesValidator {
1616
strict: bool,
17-
bytes_mode: BytesMode,
17+
bytes_mode: ValBytesMode,
1818
}
1919

2020
impl BuildValidator for BytesValidator {
@@ -33,7 +33,7 @@ impl BuildValidator for BytesValidator {
3333
} else {
3434
Ok(Self {
3535
strict: is_strict(schema, config)?,
36-
bytes_mode: BytesMode::from_config(config)?,
36+
bytes_mode: ValBytesMode::from_config(config)?,
3737
}
3838
.into())
3939
}
@@ -62,7 +62,7 @@ impl Validator for BytesValidator {
6262
#[derive(Debug, Clone)]
6363
pub struct BytesConstrainedValidator {
6464
strict: bool,
65-
bytes_mode: BytesMode,
65+
bytes_mode: ValBytesMode,
6666
max_length: Option<usize>,
6767
min_length: Option<usize>,
6868
}
@@ -116,7 +116,7 @@ impl BytesConstrainedValidator {
116116
let py = schema.py();
117117
Ok(Self {
118118
strict: is_strict(schema, config)?,
119-
bytes_mode: BytesMode::from_config(config)?,
119+
bytes_mode: ValBytesMode::from_config(config)?,
120120
min_length: schema.get_as(intern!(py, "min_length"))?,
121121
max_length: schema.get_as(intern!(py, "max_length"))?,
122122
}

src/validators/config.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use std::borrow::Cow;
2+
use std::str::FromStr;
3+
4+
use base64::Engine;
5+
use pyo3::exceptions::PyValueError;
6+
use pyo3::types::{PyDict, PyString};
7+
use pyo3::{intern, prelude::*};
8+
9+
use crate::input::EitherBytes;
10+
use crate::serializers::config::BytesMode;
11+
use crate::tools::SchemaDict;
12+
13+
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
14+
pub struct ValBytesMode {
15+
pub ser: BytesMode,
16+
}
17+
18+
impl ValBytesMode {
19+
pub fn from_config(config: Option<&Bound<'_, PyDict>>) -> PyResult<Self> {
20+
let Some(config_dict) = config else {
21+
return Ok(Self::default());
22+
};
23+
let raw_mode = config_dict.get_as::<Bound<'_, PyString>>(intern!(config_dict.py(), "val_json_bytes"))?;
24+
let ser_mode = raw_mode.map_or_else(|| Ok(BytesMode::default()), |raw| BytesMode::from_str(&raw.to_cow()?))?;
25+
Ok(Self { ser: ser_mode })
26+
}
27+
28+
pub fn deserialize_string<'a, 'py>(&self, s: &'a str) -> PyResult<EitherBytes<'a, 'py>> {
29+
match self.ser {
30+
BytesMode::Utf8 => Ok(EitherBytes::Cow(Cow::Borrowed(s.as_bytes()))),
31+
BytesMode::Base64 => match base64::engine::general_purpose::URL_SAFE.decode(s) {
32+
Ok(bytes) => Ok(EitherBytes::from(bytes)),
33+
Err(err) => Err(PyValueError::new_err(format!("Base64 decode error: {err}"))),
34+
},
35+
BytesMode::Hex => Err(PyValueError::new_err("Hex deserialization is not supported")),
36+
}
37+
}
38+
}

src/validators/json.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::input::{EitherBytes, Input, InputType, ValidationMatch};
99
use crate::serializers::config::BytesMode;
1010
use crate::tools::SchemaDict;
1111

12+
use super::config::ValBytesMode;
1213
use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator};
1314

1415
#[derive(Debug)]
@@ -88,7 +89,7 @@ impl Validator for JsonValidator {
8889
pub fn validate_json_bytes<'a, 'py>(
8990
input: &'a (impl Input<'py> + ?Sized),
9091
) -> ValResult<ValidationMatch<EitherBytes<'a, 'py>>> {
91-
match input.validate_bytes(false, BytesMode::Utf8) {
92+
match input.validate_bytes(false, ValBytesMode { ser: BytesMode::Utf8 }) {
9293
Ok(v_match) => Ok(v_match),
9394
Err(ValError::LineErrors(e)) => Err(ValError::LineErrors(
9495
e.into_iter().map(map_bytes_error).collect::<Vec<_>>(),

src/validators/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ mod bytes;
2424
mod call;
2525
mod callable;
2626
mod chain;
27+
pub(crate) mod config;
2728
mod custom_error;
2829
mod dataclass;
2930
mod date;

src/validators/uuid.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use crate::input::ValidationMatch;
1616
use crate::serializers::config::BytesMode;
1717
use crate::tools::SchemaDict;
1818

19+
use super::config::ValBytesMode;
1920
use super::model::create_class;
2021
use super::model::force_setattr;
2122
use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, Exactness, ValidationState, Validator};
@@ -170,7 +171,7 @@ impl UuidValidator {
170171
}
171172
None => {
172173
let either_bytes = input
173-
.validate_bytes(true, BytesMode::Utf8)
174+
.validate_bytes(true, ValBytesMode { ser: BytesMode::Utf8 })
174175
.map_err(|_| ValError::new(ErrorTypeDefaults::UuidType, input))?
175176
.into_inner();
176177
let bytes_slice = either_bytes.as_slice();

tests/test_json.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ def test_json_bytes_base64_round_trip():
383383
encoded = b'"aGVsbG8="'
384384
assert to_json(data, bytes_mode='base64') == encoded
385385

386-
v = SchemaValidator({'type': 'bytes'}, {'ser_json_bytes': 'base64'})
386+
v = SchemaValidator({'type': 'bytes'}, {'val_json_bytes': 'base64'})
387387
assert v.validate_json(encoded) == data
388388

389389
with pytest.raises(ValueError):
@@ -392,6 +392,6 @@ def test_json_bytes_base64_round_trip():
392392
assert to_json({'key': data}, bytes_mode='base64') == b'{"key":"aGVsbG8="}'
393393
v = SchemaValidator(
394394
{'type': 'dict', 'keys_schema': {'type': 'str'}, 'values_schema': {'type': 'bytes'}},
395-
{'ser_json_bytes': 'base64'},
395+
{'val_json_bytes': 'base64'},
396396
)
397397
assert v.validate_json('{"key":"aGVsbG8="}') == {'key': data}

0 commit comments

Comments
 (0)