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

Trying to use an enum as a Query parameter causes a panic #1244

Open
blinsay opened this issue Jan 29, 2025 · 4 comments
Open

Trying to use an enum as a Query parameter causes a panic #1244

blinsay opened this issue Jan 29, 2025 · 4 comments

Comments

@blinsay
Copy link

blinsay commented Jan 29, 2025

I've got an enum as part of my API and tried to use it as a Query type, and immediately got a panic from dropshot while generating the API. I expected this to work, since it looks like PaginationParams and WhichParams are enums and there's no warnings in the docs.

Here's a minimal repro:

use dropshot::HttpError;
use dropshot::HttpResponseOk;
use dropshot::Query;
use dropshot::RequestContext;
use schemars::JsonSchema;
use serde::Deserialize;

fn main() {
    let description = example_api_mod::stub_api_description().unwrap();
    let openapi = description.openapi("example", "0.1.0".parse().unwrap());

    openapi.write(&mut std::io::stdout().lock()).unwrap();
}

#[derive(Deserialize, JsonSchema)]
#[serde(tag = "type")]
enum QueryParamEnum {
    Foo(String),
    Bar { n: usize },
}

#[dropshot::api_description]
trait ExampleApi {
    type Context;

    #[endpoint {
        method = GET,
        path = "/hello",
    }]
    async fn do_stuff(
        ctx: RequestContext<Self::Context>,
        query: Query<QueryParamEnum>,
    ) -> Result<HttpResponseOk<()>, HttpError>;
}

The panic dumps the schema for QueryParamEnum and suggests wrapping it in a struct:

thread 'main' panicked at /Users/benl/.cargo/registry/src/index.crates.io-6f17d22bba15001f/dropshot-0.15.1/src/schema_util.rs:35:13:
while generating schema for QueryParamEnum (parameters): invalid type: {
  "title": "QueryParamEnum",
  "oneOf": [
    {
      "type": [
        "object",
        "string"
...
}
(hint: this appears to be an enum, which needs to be wrapped in a struct)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

If I try to wrap everything in a struct like the error message suggests I do, I still get a panic.

#[derive(Deserialize, JsonSchema)]
struct QueryParam {
    inner: QueryParamEnum,
}

// snip

    #[endpoint {
        method = GET,
        path = "/hello",
    }]
    async fn do_stuff(
        ctx: RequestContext<Self::Context>,
        query: Query<QueryParam>,
    ) -> Result<HttpResponseOk<()>, HttpError>;
thread 'main' panicked at src/main.rs:9:63:
called `Result::unwrap()` on an `Err` value: ApiDescriptionBuildErrors { errors: [ApiDescriptionRegisterError { operation_id: "do_stuff", message: "for endpoint do_stuff the parameter 'inner' must have a scalar type" }] }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
@david-crespo
Copy link
Contributor

It's possible we have a bug, but I suspect the issue is that that particular enum can't be easily represented as a query string, or at least, if it were it would have to be an opinionated encoding (imagine base64ed JSON or something). How do you picture that enum being represented as a string? (I'm sure we could have a better error message here too.)

PaginationParams is not an enum.

pub struct PaginationParams<ScanParams, PageSelector>

WhichPage, however, is an enum but note that it uses flatten (this might be key) and comes with a custom deserializer:

#[serde(flatten, deserialize_with = "deserialize_whichpage")]
pub page: WhichPage<ScanParams, PageSelector>,

@blinsay
Copy link
Author

blinsay commented Jan 30, 2025

It's possible we have a bug, but I suspect the issue is that that particular enum can't be easily represented as a query string

Oops, yep, that's a bad repro. Apologies! My actual types look more like this:

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type")]
enum QueryParamEnum {
    Foo(Foo),
    Bar(Bar),
}

#[derive(Serialize, Deserialize, JsonSchema)]
struct Foo {
    thing_one: String,
}

#[derive(Serialize, Deserialize, JsonSchema)]
struct Bar {
    thing_one: String,
    thing_two: String,
}

Serialized with tag = type they look like {"type":"Foo", "thing_one":"cat"}, which I had assumed could be represented as query params like type=foo&thing_one=cat. Trying that out in the repro still gets me the same error:

thread 'main' panicked at /Users/benl/.cargo/registry/src/index.crates.io-6f17d22bba15001f/dropshot-0.15.1/src/schema_util.rs:35:13:
while generating schema for QueryParamEnum (parameters): invalid type: {
  "title": "QueryParamEnum",
  "oneOf": [
...
(hint: this appears to be an enum, which needs to be wrapped in a struct)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

WhichPage, however, is an enum but note that it uses flatten (this might be key) and comes with a custom deserializer:

Ah, I didn't notice the deserialize_with! I'll give that a closer read.

@david-crespo
Copy link
Contributor

Also try putting it in a struct with flatten on the key.

@blinsay
Copy link
Author

blinsay commented Jan 30, 2025

#[derive(Serialize, Deserialize, JsonSchema)]
struct QueryParam {
    #[serde(flatten)]
    inner: QueryParamEnum,
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type")]
enum QueryParamEnum {
    Foo(Foo),
    Bar(Bar),
}

That gets me a slightly different error with no hint in the error message:

thread 'main' panicked at /Users/benl/.cargo/registry/src/index.crates.io-6f17d22bba15001f/dropshot-0.15.1/src/schema_util.rs:35:13:
while generating schema for QueryParam (parameters): invalid subschema: {
  "oneOf": [
    {
      "type": "object",
...

I'm going to try to work around this for now with a QueryParam that just contains a serde_json::Map and serde_json::from_value. I haven't tried to actually implement a server, but a struct like this at least generates a valid openapi description.

#[derive(Serialize, Deserialize, JsonSchema)]
struct QueryParam {
    #[serde(flatten)]
    inner: serde_json::Map<String, serde_json::Value>,
}

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

No branches or pull requests

2 participants