diff --git a/utoipa-gen/src/component.rs b/utoipa-gen/src/component.rs index 90bce51d..ca4ffe56 100644 --- a/utoipa-gen/src/component.rs +++ b/utoipa-gen/src/component.rs @@ -1263,14 +1263,30 @@ impl ComponentSchema { .collect::>(); if index.is_some() { - quote_spanned! {type_path.span()=> - let _ = <#rewritten_path as utoipa::PartialSchema>::schema; - + let composed_tokens = quote! { if let Some(composed) = generics.get_mut(#index) { composed.clone() } else { <#rewritten_path as utoipa::PartialSchema>::schema() } + }; + + if default.is_some() || nullable { + quote_spanned! {type_path.span()=> + let _ = <#rewritten_path as utoipa::PartialSchema>::schema; + + utoipa::openapi::schema::OneOfBuilder::new() + #nullable_item + .item( + #composed_tokens + ) + } + } else { + quote_spanned! {type_path.span()=> + let _ = <#rewritten_path as utoipa::PartialSchema>::schema; + + #composed_tokens + } } } else { quote_spanned! {type_path.span()=> @@ -1337,14 +1353,32 @@ impl ComponentSchema { quote! { <#rewritten_path as utoipa::ToSchema>::schemas(schemas) }; let composed_or_ref = |item_tokens: TokenStream| -> TokenStream { if let Some(index) = &index { - quote_spanned! {type_path.span()=> - { - let _ = <#rewritten_path as utoipa::PartialSchema>::schema; + let composed_tokens = quote! { + if let Some(composed) = generics.get_mut(#index) { + composed.clone() + } else { + #item_tokens.into() + } + }; + + if default.is_some() || nullable { + quote_spanned! {type_path.span()=> + { + let _ = <#rewritten_path as utoipa::PartialSchema>::schema; + + utoipa::openapi::schema::OneOfBuilder::new() + #nullable_item + .item( + #composed_tokens + ) + } + } + } else { + quote_spanned! {type_path.span()=> + { + let _ = <#rewritten_path as utoipa::PartialSchema>::schema; - if let Some(composed) = generics.get_mut(#index) { - composed.clone() - } else { - #item_tokens.into() + #composed_tokens } } } diff --git a/utoipa-gen/tests/schema_generics.rs b/utoipa-gen/tests/schema_generics.rs index df06e25e..ad8818c0 100644 --- a/utoipa-gen/tests/schema_generics.rs +++ b/utoipa-gen/tests/schema_generics.rs @@ -167,6 +167,29 @@ fn schema_with_non_generic_root() { assert_json_snapshot!(api); } +#[test] +fn schema_option_generic() { + #![allow(unused)] + + #[derive(ToSchema)] + struct Top { + bounds: Bounds, + } + + #[derive(ToSchema)] + struct Bounds { + min: Option, + } + + #[derive(OpenApi)] + #[openapi(components(schemas(Top)))] + struct ApiDoc; + let mut api = ApiDoc::openapi(); + api.info = Info::new("title", "version"); + + assert_json_snapshot!(api); +} + #[test] fn derive_generic_schema_enum_variants() { #![allow(unused)] diff --git a/utoipa-gen/tests/snapshots/schema_generics__schema_option_generic.snap b/utoipa-gen/tests/snapshots/schema_generics__schema_option_generic.snap new file mode 100644 index 00000000..fce3bb47 --- /dev/null +++ b/utoipa-gen/tests/snapshots/schema_generics__schema_option_generic.snap @@ -0,0 +1,43 @@ +--- +source: utoipa-gen/tests/schema_generics.rs +expression: api +--- +{ + "openapi": "3.1.0", + "info": { + "title": "title", + "version": "version" + }, + "paths": {}, + "components": { + "schemas": { + "Bounds_i32": { + "type": "object", + "properties": { + "min": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer", + "format": "int32" + } + ] + } + } + }, + "Top": { + "type": "object", + "required": [ + "bounds" + ], + "properties": { + "bounds": { + "$ref": "#/components/schemas/Bounds_i32" + } + } + } + } + } +}