GREsau / okapi

OpenAPI (AKA Swagger) document generation for Rust projects
MIT License
621 stars 112 forks source link

How to derive JsonSchema for Json<> structs #49

Closed richardmarston closed 3 years ago

richardmarston commented 3 years ago

I am trying to use okapi with a custom Responder for a Json<> wrapped struct. I am getting this error:

   Compiling openapi-responder v0.1.0 (/home/richard/small_test_projects/openapi_responder)
error[E0277]: the trait bound `rocket::serde::json::Json<Example>: JsonSchema` is not satisfied
  --> src/main.rs:20:12
   |
20 |     inner: Json<Example>,
   |            ^^^^^^^^^^^^^ the trait `JsonSchema` is not implemented for `rocket::serde::json::Json<Example>`
   |
   = note: required by `add_schema_as_property`

Here is my Cargo.toml:

[package]
name = "openapi-responder"
version = "0.1.0"
edition = "2018"

[dependencies]
rocket = { version = "0.5.0-rc.1", features = [ "json" ] }
serde = { version = "1.0.64", features = ["derive"] }
serde_json = { version = "1.0.64" }
okapi = "0.6.0-alpha-1"
rocket_okapi = "0.7.0-alpha-1"
rocket_okapi_codegen = "0.7.0-alpha-1"
schemars = "^0.8"

Here is my main.rs:

#[macro_use]
extern crate rocket;
extern crate rocket_okapi;
use rocket_okapi::{openapi, routes_with_openapi, OpenApiError, response::OpenApiResponderInner, gen::OpenApiGenerator, util::add_schema_response};
use okapi::openapi3::Responses;

use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use schemars::JsonSchema;

#[derive(Serialize, Deserialize, JsonSchema)]
pub struct Example {
    error: String,
}

#[derive(Responder, JsonSchema)]
#[response(content_type = "json", status=200)]
pub struct ExampleResponse {
    inner: Json<Example>,
}

#[openapi]
#[get("/example_response", format="application/json")]
pub async fn get_example_response() -> ExampleResponse {
    ExampleResponse { inner: Json(Example { error: "hi".to_owned() } ) }
}

impl OpenApiResponderInner for ExampleResponse {
    fn responses(gen: &mut OpenApiGenerator) -> Result<Responses, OpenApiError>{
        let mut responses = Responses::default();
        let schema = gen.json_schema::<Json<Example>>();
        add_schema_response(&mut responses, 200, "application/json", schema)?;
        Ok(responses)
    }
}

#[rocket::main]
#[doc = "The main entry point for Rocket" ]
async fn main() -> Result <(), rocket::Error> {
    rocket::build()
        .mount("/", routes_with_openapi![get_example_response])
        .launch()
        .await
}

I don't understand how I can generate the schema for a Json struct. I can't use the derive(JsonSchema) attribute because it's not a struct, enum or union.

ralpha commented 3 years ago

I don't know exactly why you want to wrap the whole response in a structs. And easier way would be to do the following: (note example code below still uses rocket v0.4.x, so things might be slightly different, but I hope this gives you an example)

Create a type that you will (most likely) reuse for most of your API endpoint:

pub type Result<T> = std::result::Result<rocket_contrib::json::Json<T>, APIErrorNoContent>;

Where APIErrorNoContent is your own custom type of error.

You can then use OpenApiResponder to add error codes to your error type, so you can have multiple error types for direct endpoints (post, get, put,..)

For example:

impl OpenApiResponder<'_> for APIErrorNoContent {
    fn responses(_: &mut OpenApiGenerator) -> OpenApiResult<Responses> {
        let mut responses = Responses::default();
        add_204_error(&mut responses); // These functions are defined in code linked below
        add_400_error(&mut responses);
        add_404_error(&mut responses);
        add_500_error(&mut responses);
        Ok(responses)
    }
}

Example is from: https://gitlab.com/df_storyteller/df-storyteller/-/blob/master/df_st_api/src/api_errors.rs

Your endpoint mainly stays the same:

#[openapi]
#[get("/example_response", format="application/json")]
pub async fn get_example_response() -> crate::Result<Example> {
    Ok(Json(Example { error: "hi".to_owned() } ))
}

If you want more example code or look at some other things: https://gitlab.com/df_storyteller/df-storyteller/-/tree/master/df_st_api/src Here is the docs that project generates: https://docs.dfstoryteller.com/rapidoc/

I hope this helps even though it is not a direct answer to your problem. I hope this gives you some inside on how to approach this. If this does not help let me know and I'll take a look at the exact problem you are having.

richardmarston commented 3 years ago

Hi @ralpha , thank you for your reply. I took the idea of wrapping the Response in a struct from https://api.rocket.rs/master/rocket/derive.Responder.html. In particular, the example that looks like this:

#[derive(Responder)]
enum MyResponder {
    A(String),
    B(File, ContentType, #[response(ignore)] Other),
}

#[derive(Responder)]
struct MyOtherResponder {
    inner: NamedFile,
    header: ContentType,
    #[response(ignore)]
    other: Other,
}

I started working with this structure because I wanted a single Responder type that was capable of returning a different response code and a different content type depending on the validity of the request, but that would leave a neater signature on the endpoint than my previous solution of nesting the Result inside a tuple with a Status:

pub async fn my_function() -> (Status, Result<Json<Example>, String>)

I think this also had the effect of making the #[openapi] requirements more complicated but I honestly can't remember now.

I think your suggestion of customizing the Error rather than customizing the Response looks much simpler, so I'll try that tomorrow.

Thanks again.

ralpha commented 3 years ago

If you need to return different results in the "correct" case you still need to look at other solutions, in that case the one above does not work. But from my experience this is never desired when creating API's. Then you usually have 2 cases. 1 if all is okay. and 1 where there is an error.

And the way I suggested above works wonderfully in that case. You can turn the error into json with custom HTTP codes using the following code:

/// Error messages returned to user
#[derive(Debug, serde::Serialize, schemars::JsonSchema)]
pub struct Error {
    /// The title of the error message
    pub err: String,
    /// The description of the error
    pub msg: Option<String>,
    // Internal error codes
    pub code: u16,
    // HTTP Status Code returned
    #[serde(skip)]
    pub http_status_code: u16,
}

impl<'r> Responder<'r> for Error {
    fn respond_to(self, _: &Request<'_>) -> response::Result<'r> {
        Response::build()
            .sized_body(std::io::Cursor::new(
                // Convert object to json
                serde_json::to_string(&self).unwrap(),
            ))
            .header(ContentType::JSON)
            .status(Status::new(self.http_status_code, ""))
            .ok()
    }
}

I then just implement From<> to convert other types of errors to the type that is returned. (like io error, serde errors, ....)

impl From<InternalError> for Error {
    fn from(internal_error: InternalError) -> Self {
       // ...
    }
}

This way I can just use the ? in the endpoint and can just write clean code and still return nice errors if something is wrong.