serde_urlencoded icon indicating copy to clipboard operation
serde_urlencoded copied to clipboard

Serializer unable to serialize nested enums, Error:"top-level serializer supports only maps and structs"

Open Orkking2 opened this issue 1 year ago • 1 comments

I was trying to find a way to encode URL parameters into definite enums so there's no guessing if a parameter is valid or not, but the solution I figured out doesn't work :(

Here is the code and its output. As you can see, it works with the JSON serializer and formats the output into a map, but the url_encoder doesn't exhibit the same behaviour?

A possible (dirty) solution to my problem in particular would be to first serialize my custom type into a JSON value and then pass that into the URL serializer. There isn't really a problem with this solution besides being clunky and unintuitive, but I was wondering if the url_encoder could be expanded to include serialization targets that are able to be coerced into struct/map representations. Just like how the JSON output is a dictionary, so too could the url_encoder represent this case as a map.

I'm not super familiar with how serializers work, sorry 😞

use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::to_value;

#[derive(Serialize, Deserialize)]
enum Params {
  EnumA(EnumA),
  EnumB(EnumB),
}

trait IntoParam {
  fn into_param(self) -> Params;
}

// trait FromParam {
//   fn from_param(param: Params) -> Self;
// }

#[derive(Serialize, Deserialize)]
// #[serde(tag = "EnumA")]
enum EnumA {
  VarA,
  VarB,
}

impl IntoParam for EnumA {
  fn into_param(self) -> Params {
    Params::EnumA(self)
  }
}

#[derive(Serialize, Deserialize)]
// #[serde(tag = "EnumB")]
enum EnumB {
  VarC,
  #[serde(untagged)]
  Base(EnumA),
}

impl IntoParam for EnumB {
  fn into_param(self) -> Params {
    Params::EnumB(self)
  }
}

fn main() -> () {
  println!("EnumA::VarA.into_param(), {}", to_value(EnumA::VarA.into_param()).unwrap().to_string());
  println!("EnumA::VarB.into_param(), {}", to_value(EnumA::VarB.into_param()).unwrap().to_string());
  println!("EnumB::VarC.into_param(), {}", to_value(EnumB::VarC.into_param()).unwrap().to_string());
  println!("EnumB::Base(EnumA::VarA).into_param(), {}", to_value(EnumB::Base(EnumA::VarA).into_param()).unwrap().to_string());
  
  let client = Client::new();
  let request = client.get("https://google.com").query(&EnumB::Base(EnumA::VarA).into_param()).build().unwrap();
  println!("Reqwest url, {}", request.url())
}

EnumA::VarA.into_param(), {"EnumA":"VarA"} EnumA::VarB.into_param(), {"EnumA":"VarB"} EnumB::VarC.into_param(), {"EnumB":"VarC"} EnumB::Base(EnumA::VarA).into_param(), {"EnumB":"VarA"} thread 'main' panicked at src/scripts/analyze_endpoints.rs:58:104: called Result::unwrap() on an Err value: reqwest::Error { kind: Builder, source: Custom("top-level serializer supports only maps and structs") } stack backtrace: 0: rust_begin_unwind at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/panicking.rs:645:5 1: core::panicking::panic_fmt at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/panicking.rs:72:14 2: core::result::unwrap_failed at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/result.rs:1654:5 3: core::result::Result<T,E>::unwrap at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/result.rs:1077:23 4: analyze_endpoints::main at ./src/scripts/analyze_endpoints.rs:58:17 5: core::ops::function::FnOnce::call_once at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/ops/function.rs:250:5 note: Some details are omitted, run with RUST_BACKTRACE=full for a verbose backtrace.

Orkking2 avatar Jun 11 '24 18:06 Orkking2

Note that the following executes with no errors and produces the indented output:

use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::to_value;

#[derive(Serialize, Deserialize)]
enum Params {
  EnumA(EnumA),
  EnumB(EnumB),
}

trait IntoParam {
  fn into_param(self) -> Params;
}

// trait FromParam {
//   fn from_param(param: Params) -> Self;
// }

#[derive(Serialize, Deserialize)]
// #[serde(tag = "EnumA")]
enum EnumA {
  VarA,
  VarB,
}

impl IntoParam for EnumA {
  fn into_param(self) -> Params {
    Params::EnumA(self)
  }
}

#[derive(Serialize, Deserialize)]
// #[serde(tag = "EnumB")]
enum EnumB {
  VarC,
  #[serde(untagged)]
  Base(EnumA),
}

impl IntoParam for EnumB {
  fn into_param(self) -> Params {
    Params::EnumB(self)
  }
}

fn main() -> () {
  println!("EnumA::VarA.into_param(), {}", to_value(EnumA::VarA.into_param()).unwrap().to_string());
  println!("EnumA::VarB.into_param(), {}", to_value(EnumA::VarB.into_param()).unwrap().to_string());
  println!("EnumB::VarC.into_param(), {}", to_value(EnumB::VarC.into_param()).unwrap().to_string());
  println!("EnumB::Base(EnumA::VarA).into_param(), {}", to_value(EnumB::Base(EnumA::VarA).into_param()).unwrap().to_string());
  
  let client = Client::new();
  let request = client.get("https://google.com").query(&to_value(EnumB::Base(EnumA::VarA).into_param()).unwrap()).build().unwrap();
  println!("Reqwest url, {}", request.url())
}

EnumA::VarA.into_param(), {"EnumA":"VarA"} EnumA::VarB.into_param(), {"EnumA":"VarB"} EnumB::VarC.into_param(), {"EnumB":"VarC"} EnumB::Base(EnumA::VarA).into_param(), {"EnumB":"VarA"} Reqwest url, https://google.com/?EnumB=VarA

Orkking2 avatar Jun 11 '24 18:06 Orkking2