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

Pass arguments to serialize_with function #1059

Closed
dtolnay opened this issue Sep 21, 2017 · 10 comments
Closed

Pass arguments to serialize_with function #1059

dtolnay opened this issue Sep 21, 2017 · 10 comments

Comments

@dtolnay
Copy link
Member

dtolnay commented Sep 21, 2017

Currently serialize_with always takes 2 arguments, the value being serialized and the serializer, and deserialize_with always takes 1 argument, the deserializer.

In some cases it may be convenient to pass an additional argument. For example formatting to base64 with a custom base64 configuration:

const CUSTOM_B64: base64::Config = /* ... */;

#[derive(Serialize)]
struct S {
    #[serde(serialize_with = "ser_base64")] // but also pass CUSTOM_B64
    bytes: Vec<u8>,
}

fn ser_base64<S>(bytes: &[u8], serializer: S, config: base64::Config) -> Result;
@dtolnay
Copy link
Member Author

dtolnay commented Sep 21, 2017

This almost already works if you define serialize and deserialize helpers on base64::Config.

impl base64::Config {
    fn serialize<S>(&self, bytes: &[u8], serializer: S) -> Result;
}

const CUSTOM_B64: base64::Config = /* ... */;

#[derive(Serialize)]
struct S {
    #[serde(serialize_with = "CUSTOM_B64")]
    bytes: Vec<u8>,
}

Except that the generated code tries to invoke CUSTOM_B64::serialize instead of CUSTOM_B64.serialize.

@dtolnay
Copy link
Member Author

dtolnay commented Sep 21, 2017

With RFC 2000, the solution may just be wait for const generics. I think that would require no code change in Serde.

const CUSTOM_B64: base64::Config = /* ... */;

#[derive(Serialize)]
struct S {
    #[serde(serialize_with = "B64::<CUSTOM_B64>")]
    bytes: Vec<u8>,
}

struct B64<const CONFIG: base64::Config>;

impl<const CONFIG: base64::Config> B64<CONFIG> {
    fn serialize<S>(bytes: &[u8], serializer: S) -> Result;
}

jonasbb added a commit to jonasbb/serde_with that referenced this issue May 28, 2018
String based collections use Display and FromStr for de/serialization of
types. Each value is then separated by a seperator, commonly space or
comma.

A similar wish was voiced at
serde-rs/serde#581

The implementation uses something like
serde-rs/serde#1059 to support arbitrary, even
library user defined, separators.
bors bot added a commit to jonasbb/serde_with that referenced this issue May 28, 2018
4: Miscellanious changes r=jonasbb a=jonasbb

* Rename module
* Some improvements to the doc comments
* Ignore the Cargo.lock file

bors r+

5: String collections r=jonasbb a=jonasbb

String based collections use Display and FromStr for de/serialization of
types. Each value is then separated by a seperator, commonly space or
comma.

A similar wish was voiced at
serde-rs/serde#581

The implementation uses something like
serde-rs/serde#1059 to support arbitrary, even
library user defined, separators.

bors r+

Co-authored-by: Jonas Bushart <[email protected]>
@mpapierski
Copy link

mpapierski commented Aug 28, 2018

Another possibly useful use case: Use existing methods with additional parameters.

fn serialize_bigint_as_str<S>(val: &BigInt, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer
{
    serializer.serialize_str(&val.to_str_radix(10))
}

What if you could just pass:

#[derive(Serialize)]
struct Foo {
  #[serde(serialize_with="BigInt::to_str_radix(10)")]
  value: BigInt
}

How hard it would be to add an attribute that would work on an object, and would expect a valid expression such as serialize_with_obj="obj.to_str_radix(10)"?

Edit:

Or possibly add serialize_with_args=[10] that would concatenate path from serialize_with="Foo::func so it would generate Foo::func(&obj, 10)?

@Zebradil

This comment has been minimized.

@dtolnay
Copy link
Member Author

dtolnay commented Jan 6, 2019

I am closing the issue as I don't consider this important to address. The suggested workaround is writing a named function that forwards to a call to your serialization function with the right additional arguments.

fn ser_with_custom_base64<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    const CUSTOM_B64: base64::Config = /* ... */;
    ser_base64(bytes, serializer, CUSTOM_B64)
}

@dtolnay dtolnay closed this as completed Jan 6, 2019
@michaelkirk
Copy link

With the example of base64 serialization, there are probably only ever a few variation of arguments you might pass, so adding this feature is admittedly not that critical.

More painful though, is something like Date formatting, where there are potentially a lot of different arguments. It'd be nice to be able to do something like this:

#[derive(Serialize)]
struct Foo {
  #[serde(deserialize_with="parse_date_time_with_format("%Y.%M.%D %H")]
  value_1: DateTime
  #[serde(deserialize_with="parse_date_time_with_format("%r")]
  value_2: DateTime
}

Rather than something like:

#[derive(Serialize)]
struct Foo {
  // I know this example is extreme, but I hope you get the idea.
  #[serde(deserialize_with="parse_date_time_with_format_Y_dot_M_dot_D_space_H")] 
  value_1: DateTime
  #[serde(deserialize_with="parse_date_time_with_format_r)]
  value_2: DateTime
}

@Mingun
Copy link
Contributor

Mingun commented May 14, 2022

This better handle with #1550, IMHO.

@zesterer
Copy link

For anybody reading this: a neat trick that I've found for cases where the configuration data is very simple is to pass the value in as a const generic, like so:

#[derive(Serialize)]
struct Foo {
    #[serialize_with = "serialize_bar::<_, 5>"]
    bar: Bar,
}

fn serialize_bar<S: Serializer, const N: u32>(&Bar, ser: S) -> Result<S::Ok, S::Error> {
    ...
}

@michaelkirk
Copy link

Thanks for sharing @zesterer.

Once (if) feature(adt_const_params) is stabilized, we can use &'static str, which would be great.

#![feature(adt_const_params)] 

#[derive(serde::Serialize)]                                               
struct Foo {                                                             
    #[serde(serialize_with = r#"serialize_time_with_format::<_, "%Y-%m-%d">"#)]    
    year_month_day: DateTime<Utc>,    
                                                                         
    #[serde(serialize_with = r#"serialize_time_with_format::<_, "%a %b %e %T %Y">"#)]    
    human_readable: DateTime<Utc>                                        
}    
                                                                  
fn serialize_time_with_format<S: Serializer, const fmt: &'static str>(date_time: &DateTime<Utc>, ser: S) -> Result<S::Ok, S::Error> {    
    let date_str = date_time.format(fmt).to_string();             
    ser.serialize_str(&date_str)    
}   

@kangalio
Copy link

Another fix (the most straightforward, simplest, and most powerful, in my opinion) would be to allow expressions as functions, not just identifiers.

That would be consistent with Rust's design of function calls: in greet("brad"), greet is an expression and you can also write (greet)("brad") or (|name| println!("hi {}", name))("brad")

The original code snippet of this issue would be written as:

const CUSTOM_B64: base64::Config = /* ... */;

#[derive(Serialize)]
struct S {
    #[serde(serialize_with = "|b, s| ser_base64(b, s, CUSTOM_B64)")]
    bytes: Vec<u8>,
}

fn ser_base64<S>(bytes: &[u8], serializer: S, config: base64::Config) -> Result;

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Development

No branches or pull requests

7 participants