Skip to content

Commit

Permalink
Add ser_json_inf_nan='strings' mode to produce valid JSON (#1307)
Browse files Browse the repository at this point in the history
  • Loading branch information
josh-newman authored Jun 10, 2024
1 parent 1f8b30b commit 96aca52
Show file tree
Hide file tree
Showing 8 changed files with 43 additions and 17 deletions.
8 changes: 4 additions & 4 deletions python/pydantic_core/_pydantic_core.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ def to_json(
round_trip: bool = False,
timedelta_mode: Literal['iso8601', 'float'] = 'iso8601',
bytes_mode: Literal['utf8', 'base64'] = 'utf8',
inf_nan_mode: Literal['null', 'constants'] = 'constants',
inf_nan_mode: Literal['null', 'constants', 'strings'] = 'constants',
serialize_unknown: bool = False,
fallback: Callable[[Any], Any] | None = None,
serialize_as_any: bool = False,
Expand All @@ -374,7 +374,7 @@ def to_json(
round_trip: Whether to enable serialization and validation round-trip support.
timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'` or `'float'`.
bytes_mode: How to serialize `bytes` objects, either `'utf8'` or `'base64'`.
inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'` or `'constants'`.
inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`.
serialize_unknown: Attempt to serialize unknown types, `str(value)` will be used, if that fails
`"<Unserializable {value_type} object>"` will be used.
fallback: A function to call when an unknown value is encountered,
Expand Down Expand Up @@ -428,7 +428,7 @@ def to_jsonable_python(
round_trip: bool = False,
timedelta_mode: Literal['iso8601', 'float'] = 'iso8601',
bytes_mode: Literal['utf8', 'base64'] = 'utf8',
inf_nan_mode: Literal['null', 'constants'] = 'constants',
inf_nan_mode: Literal['null', 'constants', 'strings'] = 'constants',
serialize_unknown: bool = False,
fallback: Callable[[Any], Any] | None = None,
serialize_as_any: bool = False,
Expand All @@ -449,7 +449,7 @@ def to_jsonable_python(
round_trip: Whether to enable serialization and validation round-trip support.
timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'` or `'float'`.
bytes_mode: How to serialize `bytes` objects, either `'utf8'` or `'base64'`.
inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'` or `'constants'`.
inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`.
serialize_unknown: Attempt to serialize unknown types, `str(value)` will be used, if that fails
`"<Unserializable {value_type} object>"` will be used.
fallback: A function to call when an unknown value is encountered,
Expand Down
2 changes: 1 addition & 1 deletion python/pydantic_core/core_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class CoreConfig(TypedDict, total=False):
# the config options are used to customise serialization to JSON
ser_json_timedelta: Literal['iso8601', 'float'] # default: 'iso8601'
ser_json_bytes: Literal['utf8', 'base64', 'hex'] # default: 'utf8'
ser_json_inf_nan: Literal['null', 'constants'] # default: 'null'
ser_json_inf_nan: Literal['null', 'constants', 'strings'] # default: 'null'
# used to hide input data from ValidationError repr
hide_input_in_errors: bool
validation_error_cause: bool # default: False
Expand Down
1 change: 1 addition & 0 deletions src/serializers/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ serialization_mode! {
"ser_json_inf_nan",
Null => "null",
Constants => "constants",
Strings => "strings",
}

impl TimedeltaMode {
Expand Down
7 changes: 2 additions & 5 deletions src/serializers/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use pyo3::types::{PyByteArray, PyBytes, PyDict, PyFrozenSet, PyIterator, PyList,
use serde::ser::{Error, Serialize, SerializeMap, SerializeSeq, Serializer};

use crate::input::{EitherTimedelta, Int};
use crate::serializers::type_serializers;
use crate::tools::{extract_i64, py_err, safe_repr};
use crate::url::{PyMultiHostUrl, PyUrl};

Expand Down Expand Up @@ -403,11 +404,7 @@ pub(crate) fn infer_serialize_known<S: Serializer>(
ObType::Bool => serialize!(bool),
ObType::Float | ObType::FloatSubclass => {
let v = value.extract::<f64>().map_err(py_err_se_err)?;
if (v.is_nan() || v.is_infinite()) && extra.config.inf_nan_mode == InfNanMode::Null {
serializer.serialize_none()
} else {
serializer.serialize_f64(v)
}
type_serializers::float::serialize_f64(v, serializer, extra.config.inf_nan_mode.clone())
}
ObType::Decimal => value.to_string().serialize(serializer),
ObType::Str | ObType::StrSubclass => {
Expand Down
27 changes: 20 additions & 7 deletions src/serializers/type_serializers/float.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,24 @@ impl FloatSerializer {
}
}

pub fn serialize_f64<S: Serializer>(v: f64, serializer: S, inf_nan_mode: InfNanMode) -> Result<S::Ok, S::Error> {
if v.is_nan() || v.is_infinite() {
match inf_nan_mode {
InfNanMode::Null => serializer.serialize_none(),
InfNanMode::Constants => serializer.serialize_f64(v),
InfNanMode::Strings => {
if v.is_nan() {
serializer.serialize_str("NaN")
} else {
serializer.serialize_str(if v.is_sign_positive() { "Infinity" } else { "-Infinity" })
}
}
}
} else {
serializer.serialize_f64(v)
}
}

impl BuildSerializer for FloatSerializer {
const EXPECTED_TYPE: &'static str = "float";

Expand Down Expand Up @@ -85,16 +103,11 @@ impl TypeSerializer for FloatSerializer {
serializer: S,
include: Option<&Bound<'_, PyAny>>,
exclude: Option<&Bound<'_, PyAny>>,
// TODO: Merge extra.config into self.inf_nan_mode?
extra: &Extra,
) -> Result<S::Ok, S::Error> {
match value.extract::<f64>() {
Ok(v) => {
if (v.is_nan() || v.is_infinite()) && self.inf_nan_mode == InfNanMode::Null {
serializer.serialize_none()
} else {
serializer.serialize_f64(v)
}
}
Ok(v) => serialize_f64(v, serializer, self.inf_nan_mode.clone()),
Err(_) => {
extra.warnings.on_fallback_ser::<S>(self.get_name(), value, extra)?;
infer_serialize(value, serializer, include, exclude, extra)
Expand Down
8 changes: 8 additions & 0 deletions tests/serializers/test_any.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,14 @@ def test_ser_json_inf_nan_with_any() -> None:
assert s.to_python(nan, mode='json') is None
assert s.to_json(nan) == b'null'

s = SchemaSerializer(core_schema.any_schema(), core_schema.CoreConfig(ser_json_inf_nan='strings'))
assert isinf(s.to_python(inf))
assert isinf(s.to_python(inf, mode='json'))
assert s.to_json(inf) == b'"Infinity"'
assert isnan(s.to_python(nan))
assert isnan(s.to_python(nan, mode='json'))
assert s.to_json(nan) == b'"NaN"'


def test_ser_json_inf_nan_with_list_of_any() -> None:
s = SchemaSerializer(
Expand Down
3 changes: 3 additions & 0 deletions tests/serializers/test_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ def test_numpy():
(float('inf'), 'Infinity', {'ser_json_inf_nan': 'constants'}),
(float('-inf'), '-Infinity', {'ser_json_inf_nan': 'constants'}),
(float('nan'), 'NaN', {'ser_json_inf_nan': 'constants'}),
(float('inf'), '"Infinity"', {'ser_json_inf_nan': 'strings'}),
(float('-inf'), '"-Infinity"', {'ser_json_inf_nan': 'strings'}),
(float('nan'), '"NaN"', {'ser_json_inf_nan': 'strings'}),
],
)
def test_float_inf_and_nan_serializers(value, expected_json, config):
Expand Down
4 changes: 4 additions & 0 deletions tests/validators/test_float.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,10 @@ def test_allow_inf_nan_true_json() -> None:
assert v.validate_json('Infinity') == float('inf')
assert v.validate_json('-Infinity') == float('-inf')

assert v.validate_json('"NaN"') == IsFloatNan()
assert v.validate_json('"Infinity"') == float('inf')
assert v.validate_json('"-Infinity"') == float('-inf')


def test_allow_inf_nan_false_json() -> None:
v = SchemaValidator(core_schema.float_schema(), core_schema.CoreConfig(allow_inf_nan=False))
Expand Down

0 comments on commit 96aca52

Please sign in to comment.