Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow sorting keys on to_json and to_python by passing in sort_keys #1637

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions python/pydantic_core/_pydantic_core.pyi
Original file line number Diff line number Diff line change
@@ -305,6 +305,7 @@ class SchemaSerializer:
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
sort_keys: Literal['recursive', 'top-level', 'unsorted'] = 'unsorted',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need these three options? surely if we add this feature it should match python's json.dumps behaviour and be sort_keys: bool = False.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are scenarios where you'd want to only sort the top level, e.g. the trick we were doing with our JSON data in FusionFire.

But I also agree that if no one is asking for it we shouldn't implement extra stuff.

How does it sound if for now we do sort_keys: bool = False and then if needed in the future we can do sort_keys: bool | Literal['top-level'] = False.
We reduce the complexity and make a nicer signature for now but can still add the top-level option in the future, even if it makes the signature a bit weird.

@samuelcolvin @aezomz wdyt?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, that's what we've done for previous flags.

round_trip: bool = False,
warnings: bool | Literal['none', 'warn', 'error'] = True,
fallback: Callable[[Any], Any] | None = None,
@@ -326,6 +327,7 @@ class SchemaSerializer:
exclude_defaults: Whether to exclude fields that are equal to their default value.
exclude_none: Whether to exclude fields that have a value of `None`.
round_trip: Whether to enable serialization and validation round-trip support.
sort_keys: Whether to sort dictionary keys, either `'recursive'`, `'top-level'`, or `'unsorted'`.
warnings: How to handle invalid fields. False/"none" ignores them, True/"warn" logs errors,
"error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError].
fallback: A function to call when an unknown value is encountered,
@@ -352,6 +354,7 @@ class SchemaSerializer:
exclude_defaults: bool = False,
exclude_none: bool = False,
round_trip: bool = False,
sort_keys: Literal['recursive', 'top-level', 'unsorted'] = 'unsorted',
warnings: bool | Literal['none', 'warn', 'error'] = True,
fallback: Callable[[Any], Any] | None = None,
serialize_as_any: bool = False,
@@ -371,6 +374,7 @@ class SchemaSerializer:
exclude_defaults: Whether to exclude fields that are equal to their default value.
exclude_none: Whether to exclude fields that have a value of `None`.
round_trip: Whether to enable serialization and validation round-trip support.
sort_keys: Whether to sort dictionary keys, either `'recursive'`, `'top-level'`, or `'unsorted'`.
warnings: How to handle invalid fields. False/"none" ignores them, True/"warn" logs errors,
"error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError].
fallback: A function to call when an unknown value is encountered,
@@ -398,6 +402,7 @@ def to_json(
by_alias: bool = True,
exclude_none: bool = False,
round_trip: bool = False,
sort_keys: Literal['recursive', 'top-level', 'unsorted'] = 'unsorted',
timedelta_mode: Literal['iso8601', 'float'] = 'iso8601',
bytes_mode: Literal['utf8', 'base64', 'hex'] = 'utf8',
inf_nan_mode: Literal['null', 'constants', 'strings'] = 'constants',
@@ -419,6 +424,7 @@ def to_json(
by_alias: Whether to use the alias names of fields.
exclude_none: Whether to exclude fields that have a value of `None`.
round_trip: Whether to enable serialization and validation round-trip support.
sort_keys: Whether to sort dictionary keys, either `'recursive'`, `'top-level'`, or `'unsorted'`.
timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'` or `'float'`.
bytes_mode: How to serialize `bytes` objects, either `'utf8'`, `'base64'`, or `'hex'`.
inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`.
@@ -477,6 +483,7 @@ def to_jsonable_python(
by_alias: bool = True,
exclude_none: bool = False,
round_trip: bool = False,
sort_keys: Literal['recursive', 'top-level', 'unsorted'] = 'unsorted',
timedelta_mode: Literal['iso8601', 'float'] = 'iso8601',
bytes_mode: Literal['utf8', 'base64', 'hex'] = 'utf8',
inf_nan_mode: Literal['null', 'constants', 'strings'] = 'constants',
@@ -498,6 +505,7 @@ def to_jsonable_python(
by_alias: Whether to use the alias names of fields.
exclude_none: Whether to exclude fields that have a value of `None`.
round_trip: Whether to enable serialization and validation round-trip support.
sort_keys: Whether to sort dictionary keys at the root level.
timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'` or `'float'`.
bytes_mode: How to serialize `bytes` objects, either `'utf8'`, `'base64'`, or `'hex'`.
inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`.
3 changes: 3 additions & 0 deletions python/pydantic_core/core_schema.py
Original file line number Diff line number Diff line change
@@ -151,6 +151,9 @@ def serialize_as_any(self) -> bool: ...
@property
def round_trip(self) -> bool: ...

@property
def sort_keys(self) -> bool: ...

def mode_is_json(self) -> bool: ...

def __str__(self) -> str: ...
3 changes: 2 additions & 1 deletion src/errors/validation_exception.rs
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@ use crate::build_tools::py_schema_error_type;
use crate::errors::LocItem;
use crate::get_pydantic_version;
use crate::input::InputType;
use crate::serializers::{DuckTypingSerMode, Extra, SerMode, SerializationState};
use crate::serializers::{DuckTypingSerMode, Extra, SerMode, SerializationState, SortKeysMode};
use crate::tools::{safe_repr, write_truncated_to_limited_bytes, SchemaDict};

use super::line_error::ValLineError;
@@ -347,6 +347,7 @@ impl ValidationError {
None,
false,
false,
&SortKeysMode::Unsorted,
true,
None,
DuckTypingSerMode::SchemaBased,
60 changes: 60 additions & 0 deletions src/serializers/extra.rs
Original file line number Diff line number Diff line change
@@ -86,6 +86,7 @@ impl SerializationState {
by_alias: Option<bool>,
exclude_none: bool,
round_trip: bool,
sort_keys: &'py SortKeysMode,
serialize_unknown: bool,
fallback: Option<&'py Bound<'_, PyAny>>,
duck_typing_ser_mode: DuckTypingSerMode,
@@ -100,6 +101,7 @@ impl SerializationState {
false,
exclude_none,
round_trip,
sort_keys,
&self.config,
&self.rec_guard,
serialize_unknown,
@@ -126,6 +128,7 @@ pub(crate) struct Extra<'a> {
pub exclude_defaults: bool,
pub exclude_none: bool,
pub round_trip: bool,
pub sort_keys: &'a SortKeysMode,
pub config: &'a SerializationConfig,
pub rec_guard: &'a SerRecursionState,
// the next two are used for union logic
@@ -152,6 +155,7 @@ impl<'a> Extra<'a> {
exclude_defaults: bool,
exclude_none: bool,
round_trip: bool,
sort_keys: &'a SortKeysMode,
config: &'a SerializationConfig,
rec_guard: &'a SerRecursionState,
serialize_unknown: bool,
@@ -168,6 +172,7 @@ impl<'a> Extra<'a> {
exclude_defaults,
exclude_none,
round_trip,
sort_keys,
config,
rec_guard,
check: SerCheck::None,
@@ -236,6 +241,7 @@ pub(crate) struct ExtraOwned {
exclude_defaults: bool,
exclude_none: bool,
round_trip: bool,
sort_keys: SortKeysMode,
config: SerializationConfig,
rec_guard: SerRecursionState,
check: SerCheck,
@@ -257,6 +263,7 @@ impl ExtraOwned {
exclude_defaults: extra.exclude_defaults,
exclude_none: extra.exclude_none,
round_trip: extra.round_trip,
sort_keys: *extra.sort_keys,
config: extra.config.clone(),
rec_guard: extra.rec_guard.clone(),
check: extra.check,
@@ -279,6 +286,7 @@ impl ExtraOwned {
exclude_defaults: self.exclude_defaults,
exclude_none: self.exclude_none,
round_trip: self.round_trip,
sort_keys: &self.sort_keys,
config: &self.config,
rec_guard: &self.rec_guard,
check: self.check,
@@ -379,6 +387,58 @@ impl From<bool> for WarningsMode {
}
}

// #[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[derive(Debug, Clone, Copy)]
pub enum SortKeysMode {
Recursive,
TopLevel,
Unsorted,
}

impl<'py> FromPyObject<'py> for SortKeysMode {
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<SortKeysMode> {
if let Ok(str_mode) = ob.extract::<&str>() {
match str_mode {
"recursive" => Ok(Self::Recursive),
"top-level" => Ok(Self::TopLevel),
"unsorted" => Ok(Self::Unsorted),
_ => Err(PyValueError::new_err(
"Invalid sort_keys parameter, should be `'recursive'`, `'top-level'`, `'unsorted'`",
)),
}
} else {
Err(PyTypeError::new_err(
"Invalid warnings parameter, should be `'none'`, `'warn'`, `'error'` or a `bool`",
))
}
}
}

impl From<&str> for SortKeysMode {
fn from(s: &str) -> Self {
match s {
"recursive" => SortKeysMode::Recursive,
"top-level" => SortKeysMode::TopLevel,
"unsorted" => SortKeysMode::Unsorted,
_ => SortKeysMode::Unsorted,
}
}
}

impl<'py> IntoPyObject<'py> for &'_ SortKeysMode {
type Target = PyString;
type Output = Bound<'py, PyString>;
type Error = Infallible;

fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
match self {
SortKeysMode::Recursive => Ok(intern!(py, "recursive").clone()),
SortKeysMode::TopLevel => Ok(intern!(py, "top-level").clone()),
SortKeysMode::Unsorted => Ok(intern!(py, "unsorted").clone()),
}
}
}

#[cfg_attr(debug_assertions, derive(Debug))]
pub(crate) struct CollectWarnings {
mode: WarningsMode,
Loading
Oops, something went wrong.
Loading
Oops, something went wrong.