Skip to content

Commit 0205028

Browse files
committed
Add ser_json_inf_nan='strings' mode to produce valid JSON
1 parent fd26293 commit 0205028

File tree

8 files changed

+43
-17
lines changed

8 files changed

+43
-17
lines changed

python/pydantic_core/_pydantic_core.pyi

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ def to_json(
355355
round_trip: bool = False,
356356
timedelta_mode: Literal['iso8601', 'float'] = 'iso8601',
357357
bytes_mode: Literal['utf8', 'base64'] = 'utf8',
358-
inf_nan_mode: Literal['null', 'constants'] = 'constants',
358+
inf_nan_mode: Literal['null', 'constants', 'strings'] = 'constants',
359359
serialize_unknown: bool = False,
360360
fallback: Callable[[Any], Any] | None = None,
361361
serialize_as_any: bool = False,
@@ -376,7 +376,7 @@ def to_json(
376376
round_trip: Whether to enable serialization and validation round-trip support.
377377
timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'` or `'float'`.
378378
bytes_mode: How to serialize `bytes` objects, either `'utf8'` or `'base64'`.
379-
inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'` or `'constants'`.
379+
inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`.
380380
serialize_unknown: Attempt to serialize unknown types, `str(value)` will be used, if that fails
381381
`"<Unserializable {value_type} object>"` will be used.
382382
fallback: A function to call when an unknown value is encountered,
@@ -430,7 +430,7 @@ def to_jsonable_python(
430430
round_trip: bool = False,
431431
timedelta_mode: Literal['iso8601', 'float'] = 'iso8601',
432432
bytes_mode: Literal['utf8', 'base64'] = 'utf8',
433-
inf_nan_mode: Literal['null', 'constants'] = 'constants',
433+
inf_nan_mode: Literal['null', 'constants', 'strings'] = 'constants',
434434
serialize_unknown: bool = False,
435435
fallback: Callable[[Any], Any] | None = None,
436436
serialize_as_any: bool = False,
@@ -451,7 +451,7 @@ def to_jsonable_python(
451451
round_trip: Whether to enable serialization and validation round-trip support.
452452
timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'` or `'float'`.
453453
bytes_mode: How to serialize `bytes` objects, either `'utf8'` or `'base64'`.
454-
inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'` or `'constants'`.
454+
inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`.
455455
serialize_unknown: Attempt to serialize unknown types, `str(value)` will be used, if that fails
456456
`"<Unserializable {value_type} object>"` will be used.
457457
fallback: A function to call when an unknown value is encountered,

python/pydantic_core/core_schema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ class CoreConfig(TypedDict, total=False):
106106
# the config options are used to customise serialization to JSON
107107
ser_json_timedelta: Literal['iso8601', 'float'] # default: 'iso8601'
108108
ser_json_bytes: Literal['utf8', 'base64', 'hex'] # default: 'utf8'
109-
ser_json_inf_nan: Literal['null', 'constants'] # default: 'null'
109+
ser_json_inf_nan: Literal['null', 'constants', 'strings'] # default: 'null'
110110
# used to hide input data from ValidationError repr
111111
hide_input_in_errors: bool
112112
validation_error_cause: bool # default: False

src/serializers/config.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ serialization_mode! {
104104
"ser_json_inf_nan",
105105
Null => "null",
106106
Constants => "constants",
107+
Strings => "strings",
107108
}
108109

109110
impl TimedeltaMode {

src/serializers/infer.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use pyo3::types::{PyByteArray, PyBytes, PyDict, PyFrozenSet, PyIterator, PyList,
99
use serde::ser::{Error, Serialize, SerializeMap, SerializeSeq, Serializer};
1010

1111
use crate::input::{EitherTimedelta, Int};
12+
use crate::serializers::type_serializers;
1213
use crate::tools::{extract_i64, py_err, safe_repr};
1314
use crate::url::{PyMultiHostUrl, PyUrl};
1415

@@ -403,11 +404,7 @@ pub(crate) fn infer_serialize_known<S: Serializer>(
403404
ObType::Bool => serialize!(bool),
404405
ObType::Float | ObType::FloatSubclass => {
405406
let v = value.extract::<f64>().map_err(py_err_se_err)?;
406-
if (v.is_nan() || v.is_infinite()) && extra.config.inf_nan_mode == InfNanMode::Null {
407-
serializer.serialize_none()
408-
} else {
409-
serializer.serialize_f64(v)
410-
}
407+
type_serializers::float::serialize_f64(v, serializer, extra.config.inf_nan_mode.clone())
411408
}
412409
ObType::Decimal => value.to_string().serialize(serializer),
413410
ObType::Str | ObType::StrSubclass => {

src/serializers/type_serializers/float.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,24 @@ impl FloatSerializer {
3030
}
3131
}
3232

33+
pub fn serialize_f64<S: Serializer>(v: f64, serializer: S, inf_nan_mode: InfNanMode) -> Result<S::Ok, S::Error> {
34+
if v.is_nan() || v.is_infinite() {
35+
match inf_nan_mode {
36+
InfNanMode::Null => serializer.serialize_none(),
37+
InfNanMode::Constants => serializer.serialize_f64(v),
38+
InfNanMode::Strings => {
39+
if v.is_nan() {
40+
serializer.serialize_str("NaN")
41+
} else {
42+
serializer.serialize_str(if v.is_sign_positive() { "Infinity" } else { "-Infinity" })
43+
}
44+
}
45+
}
46+
} else {
47+
serializer.serialize_f64(v)
48+
}
49+
}
50+
3351
impl BuildSerializer for FloatSerializer {
3452
const EXPECTED_TYPE: &'static str = "float";
3553

@@ -85,16 +103,11 @@ impl TypeSerializer for FloatSerializer {
85103
serializer: S,
86104
include: Option<&Bound<'_, PyAny>>,
87105
exclude: Option<&Bound<'_, PyAny>>,
106+
// TODO: Merge extra.config into self.inf_nan_mode?
88107
extra: &Extra,
89108
) -> Result<S::Ok, S::Error> {
90109
match value.extract::<f64>() {
91-
Ok(v) => {
92-
if (v.is_nan() || v.is_infinite()) && self.inf_nan_mode == InfNanMode::Null {
93-
serializer.serialize_none()
94-
} else {
95-
serializer.serialize_f64(v)
96-
}
97-
}
110+
Ok(v) => serialize_f64(v, serializer, self.inf_nan_mode.clone()),
98111
Err(_) => {
99112
extra.warnings.on_fallback_ser::<S>(self.get_name(), value, extra)?;
100113
infer_serialize(value, serializer, include, exclude, extra)

tests/serializers/test_any.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,14 @@ def test_ser_json_inf_nan_with_any() -> None:
623623
assert s.to_python(nan, mode='json') is None
624624
assert s.to_json(nan) == b'null'
625625

626+
s = SchemaSerializer(core_schema.any_schema(), core_schema.CoreConfig(ser_json_inf_nan='strings'))
627+
assert isinf(s.to_python(inf))
628+
assert isinf(s.to_python(inf, mode='json'))
629+
assert s.to_json(inf) == b'"Infinity"'
630+
assert isnan(s.to_python(nan))
631+
assert isnan(s.to_python(nan, mode='json'))
632+
assert s.to_json(nan) == b'"NaN"'
633+
626634

627635
def test_ser_json_inf_nan_with_list_of_any() -> None:
628636
s = SchemaSerializer(

tests/serializers/test_simple.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ def test_numpy():
152152
(float('inf'), 'Infinity', {'ser_json_inf_nan': 'constants'}),
153153
(float('-inf'), '-Infinity', {'ser_json_inf_nan': 'constants'}),
154154
(float('nan'), 'NaN', {'ser_json_inf_nan': 'constants'}),
155+
(float('inf'), '"Infinity"', {'ser_json_inf_nan': 'strings'}),
156+
(float('-inf'), '"-Infinity"', {'ser_json_inf_nan': 'strings'}),
157+
(float('nan'), '"NaN"', {'ser_json_inf_nan': 'strings'}),
155158
],
156159
)
157160
def test_float_inf_and_nan_serializers(value, expected_json, config):

tests/validators/test_float.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,10 @@ def test_allow_inf_nan_true_json() -> None:
387387
assert v.validate_json('Infinity') == float('inf')
388388
assert v.validate_json('-Infinity') == float('-inf')
389389

390+
assert v.validate_json('"NaN"') == IsFloatNan()
391+
assert v.validate_json('"Infinity"') == float('inf')
392+
assert v.validate_json('"-Infinity"') == float('-inf')
393+
390394

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

0 commit comments

Comments
 (0)