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

Missing references for ToSchema when types are defined in dependencies #894

Closed
aminya opened this issue Apr 15, 2024 · 5 comments
Closed
Labels
wontfix This will not be worked on

Comments

@aminya
Copy link

aminya commented Apr 15, 2024

I have added for ToSchema for a MyType defined in a Cargo dependency. However, when I declare MyType as a components of the schema in my project, the fields of the third party type, have missing models.

// define in a dependency:

#[derive(Debug, Serialize, Deserialize, ToSchema)]
struct MyType {
    id: String,
    #[serde(rename = "type")]
    type_: Type,
    attributes: MyTypeAttributes,
    relationships: MyTypeRelationships,
}
// In my code:
use some_dependency::MyType;

// later:
#[derive(OpenApi)]
#[openapi(
    // ...
    components(
          schemas(MyType),
    ),
)]

Here you can see that MyTypeAttributes, MyTypeRelationships, and Type are not generated in the openapi schema.

"MyType": {
  "type": "object",
  "required": ["id", "type", "attributes", "relationships"],
  "properties": {
    "attributes": { "$ref": "#/components/schemas/models.MyTypeAttributes" }, // missing reference
    "id": { "type": "string" },
    "relationships": { "$ref": "#/components/schemas/models.MyTypeRelationships" }, // missing reference
    "type": { "$ref": "#/components/schemas/Type" } // missing reference
  }
},

It seems that the macro only goes deep one level, and doesn't consider that the fields of a struct can have schemas themselves.

@aminya
Copy link
Author

aminya commented Apr 16, 2024

I have found a hard workaround for this. I have to go through each of the fields of the dependencies and list them manually under components.

// later:
#[derive(OpenApi)]
#[openapi(
    // ...
    components(
          schemas(MyType, MyTypeAttributes, MyTypeRelationships, Type),
    ),
)]

Another issue I had is that the schema prints models.MyTypeAttributes because the type of the field is written as models::MyTypeAttributes.

So I had to write a modifier to change the opanapi.json responses

/// Remove the models. prefix from the openapi json
fn modify_openapi_json(openapi_json: &str) -> String {
  return openapi_json.replace("#/components/schemas/models.", "#/components/schemas/");
}

To print the fixed schema:

// in the main function
  let openapi = ApiDoc::openapi();
  println!("{}", modify_openapi_json(&openapi.to_json()?));

To modify the openapi response for Actix

App::new()
  .service(scope("/api/v1/openapi3.json").wrap_fn(|request, service| {
    let is_open_api = request.path() == "/api/v1/openapi3.json";
    return service.call(request).map(move |result| {
      if !is_open_api {
        return result;
      }

      // Modify the response to remove the models. prefix from the openapi json
      return result.map(|result| {
        return result.map_body(|_header, body| {
          // Extract the openapi json from the body
          let mut bytes = body
            .try_into_bytes()
            .expect("Failed to convert body to bytes");
          let body_str =
            str::from_utf8(&mut bytes).expect("Failed to convert bytes to string");

          let modified_body_str = modify_openapi_json(body_str);

          return BoxBody::new(modified_body_str);
        });
      });
    });
  }))
  .service(
    RapiDoc::with_openapi("/api/v1/openapi3.json", openapi.clone()).path("/api/v1/docs/"),
  );

@junlarsen
Copy link
Contributor

junlarsen commented Apr 17, 2024

I'm pretty sure that's the "intended" way. Utoipa doesn't include any types that don't derive ToSchema, see https://github.com/juhaku/utoipa/blob/master/utoipa/src/lib.rs#L528 for how the "standard" types are derived.

FWIW, I do the same in my project, but I didn't have to modify any paths

@juhaku
Copy link
Owner

juhaku commented May 17, 2024

As @junlarsen stated it does work as intended. However there are two separate things going on here with your particular question.

  1. Rust does not have reflection and there is no runtime evaluation that can be conditionally applied on types to see whether they have ToSchema trait implemented or not and then define the appropriate action for the type. Yet we could enforce a compile error when we see a token that is not recognized as a known type. But this would cause more trouble for users than benefit because there are cases where we cannot implement ToSchema trait for a particular type that is from third-party crate. This is then to be handled the same way as you would implement serde's Serialize and Deserializefor external types. More about the topic can be found here Creating aliases outside the context of the original struct #790 (comment)

I have found a hard workaround for this. I have to go through each of the fields of the dependencies and list them manually under components.

// later:
#[derive(OpenApi)]
#[openapi(
   // ...
   components(
         schemas(MyType, MyTypeAttributes, MyTypeRelationships, Type),
   ),
)]

This is not a workaround, that is as it is intended. Utoipa requires explicit declaration of types that suppose to be present in the OpenAPI spec. You might be interested of this crate https://github.com/ProbablyClem/utoipauto. It will enable automatic schema and path recognition. But still the types to be recognized must implement ToSchema trait.

  1. The thing about that models::MyType becoming as models.MyType is necessary for namespacing. Users might have multiple types with same name but located in different modules which need to be distinct in OpenAPI spec. In order to achieve this we need namespacing. That is if you add type with prefix like models:: you need to reference to that type with the prefix as well. Here is link to an example Using sea-orm model as body type generates incorrect schema name #435 (comment).

From the docs https://docs.rs/utoipa/latest/utoipa/derive.ToSchema.html#struct-optional-configuration-options-for-schema:

  • as = ... Can be used to define alternative path and name for the schema what will be used in the OpenAPI. E.g as = path::to::Pet. This would make the schema appear in the generated OpenAPI spec as path.to.Pet.

That is the schema path declaration used in openapi macro attribute must match to the either name of the schema or value defined with as = ... attribute of the type that implements ToSchema.

 #[openapi(
   schema(models::MyType)
 )]

@juhaku juhaku added the wontfix This will not be worked on label May 17, 2024
@juhaku juhaku closed this as completed May 22, 2024
@bennobuilder
Copy link

@juhaku Regarding Point 2. Is it possible to rename the referenced Schama name for Named Fields?

        id:
          $ref: '#/components/schemas/crate.reference_id.ReferenceId'

to

        id:
          $ref: '#/components/schemas/ReferenceId'

@juhaku
Copy link
Owner

juhaku commented Aug 28, 2024

@bennobuilder Currently you could use something like this:

struct MyType {
  #[schema(value_type = ReferenceId)]
  value: crate::reference_id::ReferenceId  
}

Above code will override the type schema and name for the generated schema.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
wontfix This will not be worked on
Projects
None yet
Development

No branches or pull requests

4 participants