serde-rs/serde

Do not serialize optional fields with a container attribute

Closed this issue ยท 16 comments

iddm commented

I know about field attribute

#[serde(skip_serializing_if = "Option::is_none")]

But it is very common, for example, for json, not to serialize fields if they are empty. I have big structures with many optional types and it is very noisy to add this attribute to each of these fields. I propose to add one container attribute instead:

#[serde(skip_optionals)]
pub struct A {
    pub field: Option<i64>,  // will not be serialized as same as if skip_serializing_if = "Option::is_none"
}
Enet4 commented

This is opportune, since there have been a few questions lately on SO around using serde with optional or null-like fields [1, 2, 3]. This includes a question of your own, it seems. ๐Ÿ™‚

I wouldn't be against a new attribute to make this use case a bit more elegant. The more I think of it, the more I realize how hard it is to come up with a good one. Nonetheless, skip_optionals seems to work fine, with the drawback that it cannot represent mandatory fields with optional values (for instance: a field that can be either string or null in JSON, but must not be missing). It also won't work with the solution here, where a custom type was used.

With all this in mind, I'm certainly interested in hearing from the Serde developers on this subject. ๐Ÿ™‚

The problem is that we have no clue which fields have what type. This is similar to the serde borrow attribute. We only get a textual representation of the types. But that could be anything... a newtype with the same name, or a type alias, or some random type which looks like a known type.

We could do some best effort thing where we try to do the right thing for vec, option, boxed slice, string...

iddm commented

@oli-obk I don't quite understand what are you gonna say. With syn crate it is very easy to enumerate fields of a struct with their types. So, we do that and mark all the options with a boolean flag which says "skip serialization if it is Option and such key was not present in the json". I will do that by myself.

The concern was things like:

type MaybeString = Option<String>;

#[derive(Serialize)]
#[serde(skip_optionals)]
struct Vityafx {
    name: MaybeString,
}

It would be unfortunate to factor out the type of a field into a type declaration (which is never a breaking change) and realize later that it broke serialization because the field no longer looks like an Option.

In any case, I would be okay with adding a container attribute that does the best-effort version of this.

iddm commented

@dtolnay oh, okay, now I agree that this would be difficult to implement since we don't know what the real type is (if syn crate gives only name of the type as String)... Is there any chance to extend syn in that way?

@vityafx it is not a limitation of syn, but of the procedural derive macros offered by rustc. This won't change in the near future, but there are vague plans to improve it someday. I'm not sure if things like this will ever work, but there will be some improvements.

It is not possible to solve in general, and I don't know of any vague plans that involve this. You might have a derive like:

#[derive(Wow)]
struct Vityafx {
    name: MaybeString,
}

where the output of the derive is:

type MaybeString = Option<String>;

So in general it is impossible to know the type of anything until after macro expansion is all done. There is no API you could invent that would tell you the type of MaybeString because that type is defined later on by the macro.

I meant improving the derive macros to not work on strings anymore and having some form of communication with the compiler other than "please dump this text somewhere near the type declaration you gave me". No clue whether that will ever help in cases like these.

Wouldn't it be possible to implement this with specialization:

trait ShouldSkip { fn should_skip(&self) -> bool }
impl<T> ShouldSkip for T {
    default fn should_skip(&self) { false }
}
impl<T> ShouldSkip for Option<T> {
    fn should_skip(&self) { self.is_none() }
}

Now you can implement the attribute as just adding #[serde(skip_serializing_if = "ShouldSkip ::should_skip")] to all the fields, regardless of their type.

Is there any update on this issue? I think Diggsey's suggestion could be used in combination with the initial suggestion of using #[serde(skip_optionals)] on the struct to achieve our goal perfectly :-)

Also adding a ping here, as we use Rust and "JSON:API" at System76. Some JSON resources in our inventory system have a dozen or so possible attributes, most of which are Option<T>, or a String / Vec that can be empty, and should not be included during serialization.

+1, All of my structures use Option fields and creating a custom type for it is extremely noisy.

I would prefer for this to be handled by an attribute macro in a different library. I posted a request for implementation in dtolnay/request-for-implementation#18.

+1 for this feature