Repository: juhaku/utoipa (Rust)

Issue:
Generic type parameters are always inlined
## Example

```rust
#[derive(ToSchema)]
pub struct ResponsePayload<T: ToSchema> {
    data: Vec<T>,
}

#[derive(ToSchema)]
pub struct PrimaryData {
    id: String,
    value: u32,
}
```

## Expected definition

```json
{
  "components": {
    "schemas": {
      "PrimaryData": {
        "properties": {
          "id": {
            "type": "string"
          },
          "value": {
            "format": "int32",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "id",
          "value"
        ],
        "type": "object"
      },
      "ResponsePayload_PrimaryData": {
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/PrimaryData"
            },
            "type": "array"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      }
    }
  },
  "info": {
    "description": "",
    "license": {
      "name": ""
    },
    "title": "utoipa-generic-response",
    "version": "0.1.0"
  },
  "openapi": "3.1.0",
  "paths": {}
}
```

## Actual definition

```json
{
  "components": {
    "schemas": {
      "PrimaryData": {
        "properties": {
          "id": {
            "type": "string"
          },
          "value": {
            "format": "int32",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "id",
          "value"
        ],
        "type": "object"
      },
      "ResponsePayload_PrimaryData": {
        "properties": {
          "data": {
            "items": {
              "properties": {
                "id": {
                  "type": "string"
                },
                "value": {
                  "format": "int32",
                  "minimum": 0,
                  "type": "integer"
                }
              },
              "required": [
                "id",
                "value"
              ],
              "type": "object"
            },
            "type": "array"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      }
    }
  },
  "info": {
    "description": "",
    "license": {
      "name": ""
    },
    "title": "utoipa-generic-response",
    "version": "0.1.0"
  },
  "openapi": "3.1.0",
  "paths": {}
}
```

```diff
25,39c25
<               "properties": {
<                 "id": {
<                   "type": "string"
<                 },
<                 "value": {
<                   "format": "int32",
<                   "minimum": 0,
<                   "type": "integer"
<                 }
<               },
<               "required": [
<                 "id",
<                 "value"
<               ],
<               "type": "object"
---
>               "$ref": "#/components/schemas/PrimaryData"
```

@juhaku is this expected? Is there a workaround? Where should I start looking if I wanted to be able to fix this or provide an alternative?

What's surprising is that traits associated types (ex: `T::SomeType`) are *not* inlined. Yet they very much depend on `T`.



Discussion: That is expected behavior indeed. There are some places where the `Inline` feature is enforced, so you might keep that in mind. But what really breaks the bank here is in the `component.rs` `compose_generics` function if I remember right. It recursively calls `PartialSchema::schema()` for all arguments eventually. And is not able to create any references. For it to be able to create references instead would probably need some changes, or perhaps changes should be done before calling this function so that it will not execute the `compose_generics` function at all.
> compose_generics function if I remember right. It recursively calls PartialSchema::schema() for all arguments eventually. And is not able to create any references. 

@juhaku looks like it's what I was looking for!

It's weird that it inlines `T` but not `T::SomeAssociatedType`. I'll start to understand why that is and see if I can avoid inlining `T`.
The generated code:

```rust
    impl<T: ToSchema> utoipa::__dev::ComposeSchema for ResponsePayload<T>
    where
        T: utoipa::ToSchema,
    {
        fn compose(
            mut generics: Vec<utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>>,
        ) -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
            {
                let mut object = utoipa::openapi::ObjectBuilder::new();
                object = object
                    .property(
                        "data",
                        utoipa::openapi::schema::ArrayBuilder::new()
                            .items({
                                let _ = <T as utoipa::PartialSchema>::schema;
                                if let Some(composed) = generics.get_mut(0usize) {
                                    std::mem::take(composed)
                                } else {
                                    utoipa::openapi::schema::RefBuilder::new()
                                        .ref_location_from_schema_name(
                                            ::alloc::__export::must_use({
                                                let res = ::alloc::fmt::format(
                                                    format_args!("{0}", <T as utoipa::ToSchema>::name()),
                                                );
                                                res
                                            }),
                                        )
                                        .into()
                                }
                            }),
                    )
                    .required("data");
                object
            }
                .into()
        }
    }
```

IMO it all comes down to the result of `generics.get_mut(0usize)`:

- If it's  `Some(_)` the type parameter is inlined.
- If it's `None`, the type parameter is turned into a `$ref`.

In my case, `generics[0]` is:

```
T(
        Object(
            Object {
                schema_type: Type(
                    Object,
                ),
                title: None,
                format: None,
                description: None,
                default: None,
                enum_values: None,
                required: [
                    "id",
                    "value",
                ],
                properties: {
                    "id": T(
                        Object(
                            Object {
                                schema_type: Type(
                                    String,
                                ),
                                title: None,
                                format: None,
                                description: None,
                                default: None,
                                enum_values: None,
                                required: [],
                                properties: {},
                                additional_properties: None,
                                property_names: None,
                                deprecated: None,
                                example: None,
                                examples: [],
                                write_only: None,
                                read_only: None,
                                xml: None,
                                multiple_of: None,
                                maximum: None,
                                minimum: None,
                                exclusive_maximum: None,
                                exclusive_minimum: None,
                                max_length: None,
                                min_length: None,
                                pattern: None,
                                max_properties: None,
                                min_properties: None,
                                extensions: None,
                                content_encoding: "",
                                content_media_type: "",
                            },
                        ),
                    ),
                    "value": T(
                        Object(
                            Object {
                                schema_type: Type(
                                    Integer,
                                ),
                                title: None,
                                format: Some(
                                    KnownFormat(
                                        Int32,
                                    ),
                                ),
                                description: None,
                                default: None,
                                enum_values: None,
                                required: [],
                                properties: {},
                                additional_properties: None,
                                property_names: None,
                                deprecated: None,
                                example: None,
                                examples: [],
                                write_only: None,
                                read_only: None,
                                xml: None,
                                multiple_of: None,
                                maximum: None,
                                minimum: Some(
                                    Float(
                                        0.0,
                                    ),
                                ),
                                exclusive_maximum: None,
                                exclusive_minimum: None,
                                max_length: None,
                                min_length: None,
                                pattern: None,
                                max_properties: None,
                                min_properties: None,
                                extensions: None,
                                content_encoding: "",
                                content_media_type: "",
                            },
                        ),
                    ),
                },
                additional_properties: None,
                property_names: None,
                deprecated: None,
                example: None,
                examples: [],
                write_only: None,
                read_only: None,
                xml: None,
                multiple_of: None,
                maximum: None,
                minimum: None,
                exclusive_maximum: None,
                exclusive_minimum: None,
                max_length: None,
                min_length: None,
                pattern: None,
                max_properties: None,
                min_properties: None,
                extensions: None,
                content_encoding: "",
                content_media_type: "",
            },
        ),
    ),
]
```

Therefore: it is inlined.

My understanding is that since it's in `generics`, it cannot be resolved to a "real" type. So a `$ref` would be a to `#/components/schemas/T`. Which does not make much sense indeed.

But when I do this:

```rust
#[derive(OpenApi)]
#[openapi(components(schemas(ResponsePayload<PrimaryData>, PrimaryData)))]
struct ApiDoc;
```

I would expect the schema generation code to have access to the `PrimaryData` type parameter in `ResponsePayload<PrimariData>`. Is that not possible @juhaku ?

Because if that is possible, then it would make sense to create a `$ref` to `PrimaryData`.
So I have a (really funny IMHO) workaround based on the fact that `T` is inlined bu `T::SomeAssociatedType` is not:

```rust
pub trait RefToSelf {
    type Ref: RefToSelf;
}

#[derive(ToSchema)]
pub struct ResponsePayload<T: ToSchema + RefToSelf<Ref = T>> {
    data: Vec<T::Ref>,
}

#[derive(ToSchema)]
pub struct PrimaryData {
    id: String,
    value: u32,
}

impl RefToSelf for PrimaryData {
    type Ref = Self;
}
```

```json
{
  "components": {
    "schemas": {
      "PrimaryData": {
        "properties": {
          "id": {
            "type": "string"
          },
          "value": {
            "format": "int32",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "id",
          "value"
        ],
        "type": "object"
      },
      "ResponsePayload_PrimaryData": {
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/PrimaryData"
            },
            "type": "array"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      }
    }
  },
  "info": {
    "description": "",
    "license": {
      "name": ""
    },
    "title": "utoipa-generic-response",
    "version": "0.1.0"
  },
  "openapi": "3.1.0",
  "paths": {}
}
```


Pretty nifty indeed.

> I would expect the schema generation code to have access to the `PrimaryData` type parameter in `ResponsePayload<PrimariData>`. Is that not possible @juhaku ?

Yes, I think it should have access to it but it is only in form of `inline` only otherwise since it is able to call the `compose` for the `PrimaryData` field. Perhaps if you remove the `inline` enforcement from the `MediaType` `get_component_schema` function call it might make it a reference.  But as you stated earlier `#/components/schemas/T` would not make reference. And it is true, that's why the `inline` is being enforced there to not to create invalid references.

In any case this change to make references instead of inlining needs to be behind a feature flag or a config option, maybe. :thinking:  
> It's weird that it inlines `T` but not `T::SomeAssociatedType`. I'll start to understand why that is and see if I can avoid inlining `T`.

That is weird indeed, I haven't tested it with the associated types but that might be because the associated types comes from the type itself and are created when `ToSchema` derive is executed for the type and not when it is used as a `request_body` or response `body`. 

In general the references are created when `ToSchema` derive executes. And the inlining happens when it is being used as request body or response body, etc. This adds to the complexity of `ComponentSchema` for it is used in both instances to create a schema for `TypeTree` of rust type definition.
> Pretty nifty indeed.

It's worth nothing that it works with `std`'s `Deref` also without a custom trait. But that's not something we want.

> In any case this change to make references instead of inlining needs to be behind a feature flag or a config option, maybe. 🤔

@juhaku can I make `ToSchema` implement `RefToSelf`? Or directly have the required associated type in `ToSchema`. This way, we would always use refs (unless `#[schema(inline)]` is specified of course).

I could hide this additional trait behind a feature flag.
Using the trick above, the impl of `ComposeSchema` for `ResponsePayload<T>` is completely different, presumably because `T::Ref` is a "known" type, not a type parameter:

```rust
    impl<T: ToSchema + RefToSelf<Target = T>> utoipa::__dev::ComposeSchema
    for ResponsePayload<T>
    where
        T: utoipa::ToSchema,
    {
        fn compose(
            mut generics: Vec<utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>>,
        ) -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
            {
                let mut object = utoipa::openapi::ObjectBuilder::new();
                object = object
                    .property(
                        "data",
                        utoipa::openapi::schema::ArrayBuilder::new()
                            .items(
                                utoipa::openapi::schema::RefBuilder::new()
                                    .ref_location_from_schema_name(
                                        ::alloc::__export::must_use({
                                            let res = ::alloc::fmt::format(
                                                format_args!("{0}", <T::Target as utoipa::ToSchema>::name()),
                                            );
                                            res
                                        }),
                                    ),
                            ),
                    )
                    .required("data");
                object
            }
                .into()
        }
    }
```

But they are, at compile time, the same type!

So if only we had a way to resolve `T` to its actual type... maybe with a special case for unit types such as:

```rust
#[derive(ToSchema)]
pub struct MyResponse(ResponsePayload<PrimaryData>);
```

Interestingly, the following change is enough to have the expected output with a ref:

```diff
diff --git a/utoipa-gen/src/component.rs b/utoipa-gen/src/component.rs
index 6a0b9b2..3fa5068 100644
--- a/utoipa-gen/src/component.rs
+++ b/utoipa-gen/src/component.rs
@@ -1255,7 +1255,7 @@ impl ComponentSchema {

                         schema.to_tokens(tokens);
                     } else {
-                        let index = container.generics.get_generic_type_param_index(type_tree);
+                        let index: Option<usize> = None;
                         // only set schema references tokens for concrete non generic types
                         if index.is_none() {
                             let reference_tokens = if let Some(children) = &type_tree.children
```

```json
{
  "components": {
    "schemas": {
      "PrimaryData": {
        "properties": {
          "id": {
            "type": "string"
          },
          "value": {
            "format": "int32",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "id",
          "value"
        ],
        "type": "object"
      },
      "ResponsePayload_PrimaryData": {
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/PrimaryData"
            },
            "type": "array"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      }
    }
  },
  "info": {
    "description": "",
    "license": {
      "name": ""
    },
    "title": "utoipa-generic-response",
    "version": "0.1.0"
  },
  "openapi": "3.1.0",
  "paths": {}
}
```

Which is both valid and what one would want.

@juhaku why are the generic types inlined then? Just to make sure their actual value doesn't have to be manually added as a schema like I do it?

```rust
#[derive(OpenApi)]
#[openapi(components(schemas(PrimaryData, ResponsePayload<PrimaryData>)))]
struct ApiDoc;
```

Or are there other edge cases I am missing?

Anyway, is it OK to simply set `index = None` when a specific cargo feature is enabled? It is certainly less hacky than my previous associated type workaround.
I have added the `force_ref_to_type_parameters` Cargo feature:

```diff
diff --git a/utoipa-gen/Cargo.toml b/utoipa-gen/Cargo.toml
index d6de157..542ed42 100644
--- a/utoipa-gen/Cargo.toml
+++ b/utoipa-gen/Cargo.toml
@@ -66,6 +66,7 @@ repr = []
 indexmap = []
 rc_schema = []
 config = ["dep:utoipa-config", "dep:once_cell"]
+force_ref_to_type_parameters = []

 # EXPERIEMENTAL! use with cauntion
 auto_into_responses = []
diff --git a/utoipa-gen/src/component.rs b/utoipa-gen/src/component.rs
index 6a0b9b2..a8839f9 100644
--- a/utoipa-gen/src/component.rs
+++ b/utoipa-gen/src/component.rs
@@ -1255,7 +1255,10 @@ impl ComponentSchema {

                         schema.to_tokens(tokens);
                     } else {
+                        #[cfg(not(feature = "force_ref_to_type_parameters"))]
                         let index = container.generics.get_generic_type_param_index(type_tree);
+                        #[cfg(feature = "force_ref_to_type_parameters")]
+                        let index: Option<usize> = None;
                         // only set schema references tokens for concrete non generic types
                         if index.is_none() {
                             let reference_tokens = if let Some(children) = &type_tree.children {
```

As explained above, it disables forced inlining for generics type parameters.

IMHO what's very cool - and that's what I am trying to achieve - is that **it makes it possible to specialize generics** to a new schema using the newtype + `#[schema(inline)]` pattern:

```rust
#[derive(ToSchema)]
pub struct MyResponse(#[schema(inline)] ResponsePayload<PrimaryData>);
```

```json
{
  "components": {
    "schemas": {
      "MyResponse": {
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/PrimaryData"
            },
            "type": "array"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "PrimaryData": {
        "properties": {
          "id": {
            "type": "string"
          },
          "value": {
            "format": "int32",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "id",
          "value"
        ],
        "type": "object"
      }
    }
  },
  "info": {
    "description": "",
    "license": {
      "name": ""
    },
    "title": "utoipa-generic-response",
    "version": "0.1.0"
  },
  "openapi": "3.1.0",
  "paths": {}
}
```

@juhaku IMO that's a good solution. Shall I make a PR for this?

Update: in my case it removes a ton of code generated by my macro. And it's Rust idiomatic. Specializing generics with newtype makes a tone of sense.

> @juhaku can I make ToSchema implement RefToSelf? Or directly have the required associated type in ToSchema. This way, we would always use refs (unless #[schema(inline)] is specified of course).

You can use the `bound` attribute to change the bounds of a type. You can find more in the docs https://docs.rs/utoipa/latest/utoipa/derive.ToSchema.html#examples. Almost very bottom there is example on how to use the `bound` attribute. 

> @juhaku why are the generic types inlined then? Just to make sure their actual value doesn't have to be manually added as a schema like I do it?

It is inlined in order to resolve correct type in every case. Also what I thought is that when generic types are used there is no need to care what the actual generic arg is but it could be just inlined directly there as in short of that generic type is form of wrapper or a factory that forms concrete types when the generic arguments are resolved.

> Anyway, is it OK to simply set index = None when a specific cargo feature is enabled? It is certainly less hacky than my previous associated type workaround.

One thing to make sure here is that what happens if you try to use a primitive type as an argument. I have a hunch that for such cases the inlining e.g. setting `index = None` is not sufficient. But maybe it works, you an try it out, if you try to use number and string as arguments.
Maybe there is a 3rd solution that is even cleaner:

- Add support for `#[schema(inline[ = bool | path_to_bool_fn])]` like it's done for `#[schema(ignore)]` (cf #1177).
- Document that type parameters are inlined by default and recommend using `#[schema(inline = false)]` if that's not the expected behavior.
- Document the generics specialization using the newtype + `#[schema(inline)]` idiom (maybe in another PR).

So my original code would become:

```rust
#[derive(ToSchema)]
pub struct ResponsePayload<T: ToSchema> {
    #[schema(inline = false)]
    data: Vec<T>,
}

#[derive(ToSchema)]
pub struct PrimaryData {
    id: String,
    value: u32,
}
```

IMO it's a good solution because:

- it leverages the existing `inline` attribute and its existing semantic
- it makes this attribute parametric, making it possible to (statically) toggle inlining.

@juhaku if that makes sense, I'll open a PR.
> Add support for #[schema(inline[ = bool | path_to_bool_fn])] like it's done for #[schema(ignore)] (cf https://github.com/juhaku/utoipa/pull/1177).

`#[schema(inline = false)]` is already supported! But the following code still inlines the type parameter:

```rust
#[derive(ToSchema)]
pub struct ResponsePayload<T: ToSchema> {
    #[schema(inline = false)]
    data: Vec<T>,
}

#[derive(ToSchema)]
pub struct PrimaryData {
    id: String,
    value: u32,
}
```

So IMO that's not the expected behavior and quite misleading.
Yeah, the inline is only considered when `ToSchema` derive macro is executed for the type. But when the type is used with generic arguments there is no access to the `#[schema(inline)]` attribute of the type's field.
> Yeah, the inline is only considered when ToSchema derive macro is executed for the type. But when the type is used with generic arguments there is no access to the #[schema(inline)] attribute of the type's field.

@juhaku I'm not sure what you mean.

What I tested is:

```rust
#[derive(ToSchema)]
pub struct ResponsePayload<T: ToSchema> {
    #[schema(inline = true)]
    data: Vec<T>,
}
```

vs

```rust
#[derive(ToSchema)]
pub struct ResponsePayload<T: ToSchema> {
    #[schema(inline = false)]
    data: Vec<T>,
}
```

And it generates exactly the same code. Yet as far as I can tell it enters [the `if is_inline` branch](https://github.com/juhaku/utoipa/blob/c45215c303ab288ed132a487c4b86b00b9513573/utoipa-gen/src/component.rs#L1219) when `#[schema(inline = true)]` is used.

So I am a bit lost now...
I mean with `inline = true` it generates schema like this: It directly calls `<T as PartialSchema>::schema()` inlining `T` to array.
```rust
impl<T: ToSchema> utoipa::__dev::ComposeSchema for ResponsePayload<T>
where
    T: utoipa::ToSchema,
{
    fn compose(
        mut generics: Vec<utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>>,
    ) -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
        {
            let mut object = utoipa::openapi::ObjectBuilder::new();
            object = object
                .property(
                    "data",
                    utoipa::openapi::schema::ArrayBuilder::new()
                        .items(<T as utoipa::PartialSchema>::schema()), // <--- here
                )
                .required("data");
            object
        }
        .into()
    }
}
```
And with `inline = false` it generates this: Here it either creates a reference to the `T` or a generic argument if generic arg has been defined.
```rust
impl<T: ToSchema> utoipa::__dev::ComposeSchema for ResponsePayload<T>
where
    T: utoipa::ToSchema,
{
    fn compose(
        mut generics: Vec<utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>>,
    ) -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
        {
            let mut object = utoipa::openapi::ObjectBuilder::new();
            object = object
                .property(
                    "data",
                    utoipa::openapi::schema::ArrayBuilder::new().items({
                        let _ = <T as utoipa::PartialSchema>::schema;
                        if let Some(composed) = generics.get_mut(0usize) {
                            std::mem::take(composed)
                        } else {
                            utoipa::openapi::schema::RefBuilder::new()
                                .ref_location_from_schema_name(format!(
                                    "{}",
                                    <T as utoipa::ToSchema>::name()
                                ))
                                .into()
                        }
                    }),
                )
                .required("data");
            object
        }
        .into()
    }
}
```
> And it generates exactly the same code. Yet as far as I can tell it enters [the if is_inline branch](https://github.com/juhaku/utoipa/blob/c45215c303ab288ed132a487c4b86b00b9513573/utoipa-gen/src/component.rs#L1219) when `#[schema(inline = true)]` is used.

The reason why it goes to `if inline` branch is, in response `body` and `request_body` as well as in `openapi(components(schemas(...)))` the `Inline` feature has been enforced so that it would generate correct schema for the generic argument inlining it directly with the `compose_gnerics` function in `component.rs`

If you use the `ResponsePayload<String>` for example as an argument to the `OpenApi` derive like so:
```rust
#[derive(OpenApi)]
#[openapi(components(schemas(ResponsePayload<String>)))]
struct Api;
```
You will get following output. Note the `ResponsePayload` schema call. In order to compose the `ResponsePayload_String` type it goes to the inline branch and composes the generic arguments recursively as arguments for the `ComposeSchema` implementations. Those implementations will then use the logic as can be shown in above `ComposeSchema` implementation to get the correct type from the generic arguments of the `ResponsePayload`.
```rust
                        <ResponsePayload<String> as utoipa::__dev::ComposeSchema>::compose(
                            [<String as utoipa::PartialSchema>::schema()].to_vec(),
                        ),
```
```rust
impl utoipa::OpenApi for Api {
    fn openapi() -> utoipa::openapi::OpenApi {
        use utoipa::{Path, ToSchema};
        let mut openapi = utoipa::openapi::OpenApiBuilder::new()
            .info(
                // ... omitted 
            )
            .paths({ utoipa::openapi::path::PathsBuilder::new() })
            .components(Some(
                utoipa::openapi::ComponentsBuilder::new()
                    .schemas_from_iter({
                        let mut schemas = Vec::<(
                            String,
                            utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
                        )>::new();
                        <ResponsePayload<String> as utoipa::ToSchema>::schemas(&mut schemas);
                        schemas
                    })
                    .schema(
                        std::borrow::Cow::Owned(format!(
                            "{}_{}",
                            <ResponsePayload<String> as utoipa::ToSchema>::name(),
                            std::borrow::Cow::<String>::Owned(
                                [<String as utoipa::ToSchema>::name(),].to_vec().join("_")
                            )
                        )),
                        <ResponsePayload<String> as utoipa::__dev::ComposeSchema>::compose(
                            [<String as utoipa::PartialSchema>::schema()].to_vec(),
                        ),
                    )
                    .build(),
            ))
            .build();
        let components = openapi
            .components
            .get_or_insert(utoipa::openapi::Components::new());
        let mut schemas = Vec::<(
            String,
            utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
        )>::new();
        components.schemas.extend(schemas);
        let _mods: [&dyn utoipa::Modify; 0usize] = [];
        _mods
            .iter()
            .for_each(|modifier| modifier.modify(&mut openapi));
        openapi
    }
}
```

Maybe the `Inline` enforcement can be set configurable for `components(schemas(...))` definition and as well to `request_body` and responses `body` definition in `#[utoipa::path(...)]`
> the Inline feature has been enforced so that it would generate correct schema for the generic argument inlining it directly with the compose_gnerics function in component.rs

@juhaku where is that done?
For component it is done in `openapi.rs`
https://github.com/juhaku/utoipa/blob/c45215c303ab288ed132a487c4b86b00b9513573/utoipa-gen/src/openapi.rs#L151-L163

For `request_body` and response `body` schema references it is done here in `media_type.rs`
https://github.com/juhaku/utoipa/blob/c45215c303ab288ed132a487c4b86b00b9513573/utoipa-gen/src/path/media_type.rs#L287-L293
But maybe for schema references it can just be inline enforced? :thinking: 
> For component it is done in openapi.rs

I tried the following:

```rust
impl Schema {
    fn get_component(&self) -> Result<ComponentSchema, Diagnostics> {
        let ty = syn::Type::Path(self.0.clone());
        let type_tree = TypeTree::from_type(&ty)?;
        let generics = type_tree.get_path_generics()?;

        let container = Container {
            generics: &generics,
        };
        let component_schema = ComponentSchema::new(crate::component::ComponentSchemaProps {
            container: &container,
            type_tree: &type_tree,
            features: vec![],
            description: None,
        })?;

        Ok(component_schema)
    }
}
```

Now `#[schema(inline = true)]` is ignored *entirely* on *any* field.

But with the original code, `#[schema(inline)]` is supported as expected. Except for the fields types with generics' type parameter (ex: `Vec<T>`).

> But maybe for schema references it can just be inline enforced? 🤔

You were right: this solution does not work for native types (ex: `u32`): it will create a `$ref` to `#/components/schemas/u32`.

IMHO the behavior should be as follow:

- If it's a native type, force `inline = true` except if `#[schema(inline = false)]` is specified (because if it is specified, maybe someone does actually want to make their own `u32` schema?).
- If `#[schema(inline = ...)]` is specified, use it.
- Otherwise, assume `#[schema(inline = true)]` (in order to keep the existing behavior).

But I have no idea how to tackle this since:

1. `features: vec![],` in `openapi.rs` kinda breaks `#[schema(inline)]` entirely.
2. I'm not sure how to check for a native type.

I will eventually find for 2. I've seen some code around already in the code base.

But 1. is bugging me... Any help understanding this would be greatly appreciated.
Yup,
2.  is really simple you can use `SchemaType` to check is it primitive `SchemaType { path, is_inline }.is_primitive()` Then based on that inline or not But that should also happen within `ComponentSchema` as well because if it only happens here in `openapi.rs` it will work only of the immediate child schema but not for inner generic schema. I guess this needs some debugging indeed to see how it behaves.

Ah, true for the 1. `vec![]` in `openapi.rs` it cannot be done there. And if the `Inline` is there it will get to the `if inline` branch in `ComponentSchema` anyways.

Only place then where changes can be made with the current behavior is that branch itself. Or there is perhaps need for some architectural changes for better support for different type of generic args.
@juhaku can we agree on this: 

> IMHO the behavior should be as follow:
> 
> - If it's a native type, force `inline = true` except if `#[schema(inline = false)`] is specified (because if it is specified, maybe someone does actually want to make their own `u32` schema?).
> - If `#[schema(inline = ...)]` is specified, use it.
> - Otherwise, assume `#[schema(inline = true)]` (in order to keep the existing behavior).

Then we'll see what needs to be changed.
@JMLX42 Yeah, seems quite right to me.
Ok cool 😎 

Now how comes [this patch](https://github.com/juhaku/utoipa/issues/1182#issuecomment-2453492580) works when it's in the `else` branch?
I believe the else branch will only get executed when `ToSchema` executes for the type, It will never get to the else branch when type is declared as response body or request body or within `#[openapi(components(schemas(...)))]`.
Ok so:

- the [`if inline`](https://github.com/juhaku/utoipa/blob/c45215c303ab288ed132a487c4b86b00b9513573/utoipa-gen/src/component.rs#L1218) branch matches `#[schema(inline)]` or [the hard-coded `Feature::Inline(true.into())`](https://github.com/juhaku/utoipa/blob/c45215c303ab288ed132a487c4b86b00b9513573/utoipa-gen/src/openapi.rs#L151-L163)
- the [`else`](https://github.com/juhaku/utoipa/blob/c45215c303ab288ed132a487c4b86b00b9513573/utoipa-gen/src/component.rs#L1257) branch matches with a missing `#[schema(inline)]` or `#[schema(inline = false)]`, **but** it will inline a generic type parameter anyway

So I guess a first step would be to change the `else` branch to skip inlining except for native types.
> So I guess a first step would be to change the `else` branch to skip inlining except for native types.

Yup, that should be quite simple. I think you could do something like this in the else block:
```rust
let is_primitive = SchemaType { path: Cow::Borrowed(&rewritten_path), nullable: false}.is_primitive();
if index.is_none() && !is_primitive {...}
```



Fix (patch):
diff --git a/utoipa-gen/src/component.rs b/utoipa-gen/src/component.rs
index 6a0b9b2c..45808ad9 100644
--- a/utoipa-gen/src/component.rs
+++ b/utoipa-gen/src/component.rs
@@ -1216,6 +1216,17 @@ impl ComponentSchema {
                     let title_tokens = as_tokens_or_diagnostics!(&title);
 
                     if is_inline {
+                        let schema_type = SchemaType {
+                            path: Cow::Borrowed(&rewritten_path),
+                            nullable,
+                        };
+                        let index =
+                            if !schema_type.is_primitive() || type_tree.generic_type.is_none() {
+                                container.generics.get_generic_type_param_index(type_tree)
+                            } else {
+                                None
+                            };
+
                         object_schema_reference.is_inline = true;
                         let items_tokens = if let Some(children) = &type_tree.children {
                             schema_references.extend(Self::compose_child_references(children)?);
@@ -1223,8 +1234,21 @@ impl ComponentSchema {
                             let composed_generics =
                                 Self::compose_generics(children, container.generics)?
                                     .collect::<Array<_>>();
-                            quote_spanned! {type_path.span()=>
-                                <#rewritten_path as utoipa::__dev::ComposeSchema>::compose(#composed_generics.to_vec())
+
+                            if index.is_some() {
+                                quote_spanned! {type_path.span()=>
+                                    let _ = <#rewritten_path as utoipa::PartialSchema>::schema;
+
+                                    if let Some(composed) = generics.get_mut(#index) {
+                                        composed.clone()
+                                    } else {
+                                        <#rewritten_path as utoipa::PartialSchema>::schema()
+                                    }
+                                }
+                            } else {
+                                quote_spanned! {type_path.span()=>
+                                    <#rewritten_path as utoipa::__dev::ComposeSchema>::compose(#composed_generics.to_vec())
+                                }
                             }
                         } else {
                             quote_spanned! {type_path.span()=>
@@ -1255,8 +1279,18 @@ impl ComponentSchema {
 
                         schema.to_tokens(tokens);
                     } else {
-                        let index = container.generics.get_generic_type_param_index(type_tree);
-                        // only set schema references tokens for concrete non generic types
+                        let schema_type = SchemaType {
+                            path: Cow::Borrowed(&rewritten_path),
+                            nullable,
+                        };
+                        let index =
+                            if !schema_type.is_primitive() || type_tree.generic_type.is_none() {
+                                container.generics.get_generic_type_param_index(type_tree)
+                            } else {
+                                None
+                            };
+
+                        // forcibly inline primitive type parameters, otherwise use references
                         if index.is_none() {
                             let reference_tokens = if let Some(children) = &type_tree.children {
                                 let composed_generics = Self::compose_generics(
@@ -1281,7 +1315,7 @@ impl ComponentSchema {
                                         let _ = <#rewritten_path as utoipa::PartialSchema>::schema;
 
                                         if let Some(composed) = generics.get_mut(#index) {
-                                            std::mem::take(composed)
+                                            composed.clone()
                                         } else {
                                             #item_tokens.into()
                                         }