Repository: juhaku/utoipa (Rust)

Issue:
ToSchema aliasing on rust types
Hello all. I am trying out utoipa for the first time and was trying to create a Schema of a struct with a generic type. I was trying to follow the aliases way of handling this in the [documentation for the ToSchema macro](https://docs.rs/utoipa/latest/utoipa/derive.ToSchema.html#generic-schemas-with-aliases) but ran into some issues. In particular, the example provided does not seem to have the effect that I would expect.

This is a slightly modified version of the example provided in the documentation. 
```rust
use utoipa::{ToSchema, OpenApi};

#[derive(ToSchema)]
#[aliases(StatusMessage = Status<String>, StatusNumber = Status<i32>, StatusMore = Status<More>)]
struct Status<T> {
    value: T
}

/// An additional struct to demonstrate the differences between primitive types and defined structs
/// in the aliasing
#[derive(ToSchema)]
struct More {
    /// some inner information
    inner: String,
}

#[derive(OpenApi)]
#[openapi(
    components(schemas(StatusMessage, StatusNumber, StatusMore, More))
)]
struct ApiDoc;

fn main() {
    let doc = ApiDoc::openapi().to_pretty_json().expect("Unable to convert API docs to json");
    println!("{}", doc);
}
```

And this is the generated openapi doc from the code above (using utoipa version 2.3.0):
```json
{
  "openapi": "3.0.3",
  "info": {
    "title": "rust-playground",
    "description": "",
    "contact": {
      "name": ""
    },
    "license": {
      "name": ""
    },
    "version": "0.1.0"
  },
  "paths": {},
  "components": {
    "schemas": {
      "More": {
        "type": "object",
        "description": "An additional struct to demonstrate the differences between primitive types and defined structs",
        "required": [
          "inner"
        ],
        "properties": {
          "inner": {
            "type": "string",
            "description": "some inner information"
          }
        }
      },
      "StatusMessage": {
        "type": "object",
        "required": [
          "value"
        ],
        "properties": {
          "value": {
            "$ref": "#/components/schemas/String"
          }
        }
      },
      "StatusMore": {
        "type": "object",
        "required": [
          "value"
        ],
        "properties": {
          "value": {
            "$ref": "#/components/schemas/More"
          }
        }
      },
      "StatusNumber": {
        "type": "object",
        "required": [
          "value"
        ],
        "properties": {
          "value": {
            "$ref": "#/components/schemas/i32"
          }
        }
      }
    }
  }
}
```

The custom struct I added `More` is handled appropriately as it has it's own schema. However, the rust types `String` and `i32` don't seem to be handled properly. utoipa generates an openapi doc that suggests that `String` and `i32` are their own schemas that should have their own entry rather than being `"type": "string"`. 

Is this a bug with utoipa, or am I misinterpreting how to use the aliases properly?


Discussion:
That seems like a bug. :O I need to check this out.. The primitive types should be inlined though. 
I ran into this same problem just now.

The way I solved it:

```
// First, add a trait to convert primitive types to the correct SchemaType

pub trait SchemaTyper {
    fn schema_type() -> utoipa::openapi::SchemaType;
}

impl SchemaTyper for String {
    fn schema_type() -> utoipa::openapi::SchemaType {
        utoipa::openapi::SchemaType::String
    }
}

impl SchemaTyper for i64 {
    fn schema_type() -> utoipa::openapi::SchemaType {
        utoipa::openapi::SchemaType::Integer
    }
}

impl SchemaTyper for f64 {
    fn schema_type() -> utoipa::openapi::SchemaType {
        utoipa::openapi::SchemaType::Number
    }
}

impl SchemaTyper for bool {
    fn schema_type() -> utoipa::openapi::SchemaType {
        utoipa::openapi::SchemaType::Boolean
    }
}

// add more impls

// then, create a helper that can be used in value_type

pub struct SchemaTypeHelper<T: SchemaTyper> {
    hack: std::marker::PhantomData<T>,
}


impl<T: SchemaTyper> ToSchema for SchemaTypeHelper<T> {
    fn schema() -> utoipa::openapi::schema::Schema {
        utoipa::openapi::ObjectBuilder::new()
            .schema_type(T::schema_type())
            .into()
    }
}

// modify the generic struct to use these:

#[derive(Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct FormField<T: SchemaTyper> {
    #[schema(inline, value_type = Option<SchemaTypeHelper<T>>)]
    pub default: Option<T>,
}

// and create the aliases manually:

pub type FormFieldString = FormField<String>;
pub type FormFieldInt = FormField<i64>;
pub type FormFieldFloat = FormField<f64>;
pub type FormFieldBool = FormField<bool>;
```

Since `Toschema` isn't implemented for primitive types, I had to use a generic helper and value_type.
@nkovacs, Thank you for this workaround. I modified your solution slightly to work for my use case as my generics are over types that also implement ToSchema on their own. My solution looks something like this: 

```rust
use utoipa::{ToSchema, OpenApi};

/// An additional struct to demonstrate the differences between primitive types and defined structs
/// in the aliasing
#[derive(ToSchema)]
struct More {
    /// some inner information
    inner: String,
}

pub trait CustomSchema {
    /// Returns the schema for the type implementing this 
    fn schema() -> utoipa::openapi::Schema;
}

struct CustomSchemaHelper<T: CustomSchema> {
    hack: std::marker::PhantomData<T>,
}

impl<T: CustomSchema> ToSchema for CustomSchemaHelper<T> {
    fn schema() -> utoipa::openapi::schema::Schema {
        <T as CustomSchema>::schema()
    }
}

impl CustomSchema for String {
    fn schema() -> utoipa::openapi::Schema {
        utoipa::openapi::ObjectBuilder::new()
            .schema_type(utoipa::openapi::SchemaType::String)
            .into()
    }
}

impl CustomSchema for i32 {
    fn schema() -> utoipa::openapi::Schema {
        utoipa::openapi::ObjectBuilder::new()
            .schema_type(utoipa::openapi::SchemaType::Number)
            .into()
    }
}

impl CustomSchema for More {
    fn schema() -> utoipa::openapi::Schema {
        <More as utoipa::ToSchema>::schema()
    }
}

type StatusMessage = Status<String>;
type StatusNumber = Status<i32>;
type StatusMore = Status<More>;

#[derive(ToSchema)]
struct Status<T: CustomSchema> {
    #[schema(inline, value_type = CustomSchemaHelper<T>)]
    value: T
}

#[derive(OpenApi)]
#[openapi(
    components(schemas(StatusMessage, StatusNumber, StatusMore, More))
)]
struct ApiDoc;

fn main() {
    let doc = ApiDoc::openapi().to_pretty_json().expect("Unable to convert API docs to json");
    println!("{}", doc);
}
```
Produces
```json
{
  "openapi": "3.0.3",
  "info": {
    "title": "rust-playground",
    "description": "",
    "contact": {
      "name": ""
    },
    "license": {
      "name": ""
    },
    "version": "0.1.0"
  },
  "paths": {},
  "components": {
    "schemas": {
      "More": {
        "type": "object",
        "description": "An additional struct to demonstrate the differences between primitive types and defined structs",
        "required": [
          "inner"
        ],
        "properties": {
          "inner": {
            "type": "string",
            "description": "some inner information"
          }
        }
      },
      "StatusMessage": {
        "type": "object",
        "required": [
          "value"
        ],
        "properties": {
          "value": {
            "type": "string"
          }
        }
      },
      "StatusMore": {
        "type": "object",
        "required": [
          "value"
        ],
        "properties": {
          "value": {
            "type": "object",
            "description": "An additional struct to demonstrate the differences between primitive types and defined structs",
            "required": [
              "inner"
            ],
            "properties": {
              "inner": {
                "type": "string",
                "description": "some inner information"
              }
            }
          }
        }
      },
      "StatusNumber": {
        "type": "object",
        "required": [
          "value"
        ],
        "properties": {
          "value": {
            "type": "number"
          }
        }
      }
    }
  }
}
```

It isn't perfect since the OpenAPI output doesn't properly reference the custom `More` schema but it at least has the proper information there. 
Yeah, unfortunately for that you'd have to implement ToSchema manually for Status. The problem is that the derive generates `<CustomSchemaHelper<T> as utoipa::ToSchema>::schema()` for the value field if you use inline (and you have to use inline, otherwise it will reference a "CustomSchemaHelper" schema, which won't work), and you can't return `Ref` from `CustomSchemaHelper`'s `schema()`. We'd need a different attribute that accepts a function that returns `RefOr<Schema>`.
I think one could work-around this by using a newtype crate such as https://github.com/bbqsrc/nova - I will experiment with this but would be great if we could have a fix in utoipa instead.
I need to investigate this when I get time to and I'll report here when I know more. If there is some fix that can be done I am happy to include it to the crate. :slightly_smiling_face: 
This is indeed a bug, I fill try to apply a fix asap. 
@fdoyon @nkovacs @michael-mark Fix is on they way and soon to be merged.



Patch:
diff --git a/utoipa-gen/src/component.rs b/utoipa-gen/src/component.rs
index 48ee2e5d..9e1eeaa9 100644
--- a/utoipa-gen/src/component.rs
+++ b/utoipa-gen/src/component.rs
@@ -256,8 +256,26 @@ impl<'t> TypeTree<'t> {
         }
     }
 
-    fn update_path(&mut self, ident: &'_ Ident) {
-        self.path = Some(Cow::Owned(Path::from(ident.clone())))
+    /// Update current [`TypeTree`] from given `ident`.
+    /// 
+    /// It will update everything else except `children` for the `TypeTree`. This means that the
+    /// `TypeTree` will not be changed and will be travelsed as before update.
+    fn update(&mut self, ident: Ident) {
+        let new_path = Path::from(ident);
+
+        let segments = &new_path.segments;
+        let last_segment = segments.last().expect("TypeTree::update path should have at least one segment");
+
+        let generic_type = Self::get_generic_type(last_segment);
+        let value_type = if SchemaType(&new_path).is_primitive() {
+            ValueType::Primitive
+        } else {
+            ValueType::Object
+        };
+
+        self.value_type = value_type;
+        self.generic_type = generic_type;
+        self.path = Some(Cow::Owned(new_path));
     }
 
     /// `Object` virtual type is used when generic object is required in OpenAPI spec. Typically used
diff --git a/utoipa-gen/src/component/schema.rs b/utoipa-gen/src/component/schema.rs
index b29aabfe..cfd266ff 100644
--- a/utoipa-gen/src/component/schema.rs
+++ b/utoipa-gen/src/component/schema.rs
@@ -114,11 +114,7 @@ impl ToTokens for Schema<'_> {
                         #vis type #name = #ty #alias_type_generics;
                     }
                 })
-                .fold(quote! {}, |mut tokens, alias| {
-                    tokens.extend(alias);
-
-                    tokens
-                })
+                .collect::<TokenStream>()
         });
 
         tokens.extend(quote! {
@@ -255,8 +251,15 @@ impl NamedStructSchema<'_> {
                 .enumerate()
                 .for_each(|(index, generic)| {
                     if let Some(generic_type) = type_tree.find_mut_by_ident(&generic.ident) {
-                        generic_type
-                            .update_path(&alias.generics.type_params().nth(index).unwrap().ident);
+                        generic_type.update(
+                            alias
+                                .generics
+                                .type_params()
+                                .nth(index)
+                                .unwrap()
+                                .ident
+                                .clone(),
+                        );
                     };
                 })
         }


Test patch:
diff --git a/utoipa-gen/tests/schema_derive_test.rs b/utoipa-gen/tests/schema_derive_test.rs
index 55b44ac2..1554ae3e 100644
--- a/utoipa-gen/tests/schema_derive_test.rs
+++ b/utoipa-gen/tests/schema_derive_test.rs
@@ -2270,6 +2270,50 @@ fn derive_component_with_aliases() {
     }
 }
 
+#[test]
+fn derive_component_with_primitive_aliases() {
+    #[derive(Debug, OpenApi)]
+    #[openapi(components(schemas(BarString, BarInt)))]
+    struct ApiDoc;
+
+    #[derive(ToSchema)]
+    #[aliases(BarString = Bar<String>, BarInt = Bar<i32>)]
+    struct Bar<R> {
+        #[allow(dead_code)]
+        bar: R,
+    }
+
+    let doc = ApiDoc::openapi();
+    let doc_value = &serde_json::to_value(doc).unwrap();
+
+    let value = doc_value.pointer("/components/schemas").unwrap();
+
+    assert_json_eq!(
+        value,
+        json!({
+            "BarString": {
+                "properties": {
+                    "bar": {
+                        "type": "string"
+                    }
+                },
+                "type": "object",
+                "required": ["bar"]
+            },
+            "BarInt": {
+                "properties": {
+                    "bar": {
+                        "type": "integer",
+                        "format": "int32",
+                    }
+               },
+                "type": "object",
+                "required": ["bar"]
+            }
+        })
+    )
+}
+
 #[test]
 fn derive_component_with_into_params_value_type() {
     #[derive(ToSchema)]
@@ -2704,8 +2748,6 @@ fn derive_struct_field_with_example() {
         }
     };
 
-    dbg!(&doc);
-
     assert_json_eq!(
         doc,
         json!({