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 integer renames for struct fields #2209

Open
wants to merge 10 commits into
base: master
Choose a base branch
from

Conversation

AndrewScull
Copy link

Closes #1773 and related to PR #2056.

Extends the serde_rename and alias annotations on struct fields to support integer names. Any struct with an integer name is serialized as a map, similarly to structs with any flattened fields, avoiding the need for changes in existing serializer implementations.

Integer names are typed and serialized based on that type, but deserialization is only based on the integer value because deserializers often don't know the exact type of integers.

There are a few unrelated refactors at the start of the series as I was learning the project, then the bulk of the changes are to represent non-string renames (similar to #2056) and to handle the cases of deserialization.

Other fields are collected for structs that have flattened members. Save that
condition and to a helpfully names variable rather than doing the compound test
each time.

No functional change.
There is only one case that other inputs are ignored so rename the variable to
something that doesn't suggest the other values are always unimportant.

No functional change.
Move index, constructor and fallthrough preparation to the scope they are used
to make the function easier to read.

No functional change.
To be consistent across all types, use the fallthough to collect non-string
field names rather than having different logic between the types.

When collect_other_fields is true, it's already assumed that the fallthrough is
going to expect __value to be a variant of Content. That assumption is now
applied all the types, not just the string-like types.
Sometimes a struct is encoded as a map. This is currently only when the struct
contains a flattened field but will also be the case when non-string names are
supported. Add a predicate to determine whether a struct should be encoded as a
map so the logic can be easily updated.

No functional change.
All names are still strings, but the enum can be extended to handle names of
different types.
Track whether a container has items that are renames to integer values. If this
is the case for a struct, it will need to be serialized as a map. There's not
currently support for integer renames so this value is always false.
Checking for unknown fields, duplicate fields and missing fields that are have
integer renames.
The existing #[serde(rename = ...)] syntax is extended to support interger
names for struct fields. The type of the literal informs the serialization
function that is used for the field name with the default being i32, to match
the Rust's default.

Deserialization is more flexible and accepts any integer type, matching against
the field values as 64-bit integers. This allows better compatibility with
deserializers that don't know the exact bit width of integer values as well as
those that do.
Test integer renames of struct fields including the interactions with flattened
fields and the denial of unknown fields.
Copy link

@maurer maurer left a comment

Choose a reason for hiding this comment

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

LGTM. I noted a couple minor things, but it seems fine as is to me.

@@ -2246,70 +2275,138 @@ fn deserialize_identifier(
None
};

let visitor_helper_impl = if collect_other_fields || has_int_rename {
Copy link

Choose a reason for hiding this comment

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

Maybe "int_field_resolver" or something to make it more clear what this is? "helper" is awfully vague

@@ -100,6 +101,12 @@ impl<'a> Container<'a> {
has_flatten = true;
}
field.attrs.rename_by_rules(attrs.rename_all_rules());
if field.attrs.name().serialize_name().is_int()
Copy link

Choose a reason for hiding this comment

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

has_int_rename = x || y || z?

@@ -2,8 +2,12 @@ use serde_derive::Serialize;

#[derive(Serialize)]
struct S {
#[serde(rename = 100)]
integer: (),
#[serde(rename = 100i128)]
Copy link

Choose a reason for hiding this comment

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

I think it might be nice to squash the test patch into this one, since it's a bit odd to have negative tests but no positive tests.

@@ -643,6 +643,352 @@ fn test_unknown_field_rename_struct() {
);
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename = "IgnoredRename")]
struct IntRenameStruct {
Copy link

Choose a reason for hiding this comment

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

I think it will work as is, but it might be nice to have a test for renames to hex/binary/octal just in case.

@Progdrasil
Copy link

This would be very useful, is there anyway that I could help to move this along if @AndrewScull is too busy?

@fahimmehraj
Copy link

This is very useful for my specific use case, where I have an enum to be serialized with adjacent tags. The tags however, are numbers.
The data I want to serialize looks like this:

{
  "op": 8,
  "d": {
    "v": 4,
    "heartbeat_interval" :13750.0
  }
}

This is the code that doesn't work unfortunately:

#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "op", content = "d")]
pub enum DiscordPayload {
    #[serde(rename = "8")]
    Hello(Hello),
}

It expects an identifier rather than an integer

Error deserializing payload: invalid type: integer `8`, expected variant identifier at line 1 column 7

This change does have a valid use case!

@Progdrasil
Copy link

@frykher see #2056 for the MR pertaining to enums

nickzana added a commit to nickzana/passly that referenced this pull request May 18, 2023
serde_derive does not currently support using integer values as map
keys. CTAP uses integer values for various CBOR map keys. In order to
properly serialize and deserialize types for the CTAP protocol, a forked
version of serde from a pull request that adds support for renaming
field keys as integer types is used.

Tracked in <serde-rs/serde#2209>
@labbott
Copy link

labbott commented Dec 5, 2024

I'd love to see this completed. It would be very useful for CBOR specifications that use integers in maps.

@Progdrasil
Copy link

As an example of working around this limitation for CBOR encoding, I made this declarative macro to rename the fields to numbers for CTAP2 encoding. You can see it' use in passkey-types::ctap2::make_credential

@@ -262,7 +262,9 @@ macro_rules! declare_error_trait {
/// Raised when a `Deserialize` struct type received a field with an
/// unrecognized name.
#[cold]
fn unknown_field(field: &str, expected: &'static [&'static str]) -> Self {
fn unknown_field<T>(field: T, expected: &'static [&'static str]) -> Self
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this is backwards compatible

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

Allow integers (or custom types) to be used as names/keys
6 participants