diff --git a/utoipa-gen/src/component.rs b/utoipa-gen/src/component.rs index 90bce51d..554e983e 100644 --- a/utoipa-gen/src/component.rs +++ b/utoipa-gen/src/component.rs @@ -1287,11 +1287,7 @@ impl ComponentSchema { quote! { <#rewritten_path as utoipa::ToSchema>::schemas(schemas) }; let description_tokens = description_stream.to_token_stream(); - let schema = if default.is_some() - || nullable - || title.is_some() - || !description_tokens.is_empty() - { + let schema = if nullable { quote_spanned! {type_path.span()=> utoipa::openapi::schema::OneOfBuilder::new() #nullable_item @@ -1300,6 +1296,20 @@ impl ComponentSchema { #default_tokens #description_stream } + } else if default.is_some() + || title.is_some() + || !description_tokens.is_empty() + { + quote_spanned! {type_path.span()=> + utoipa::openapi::schema::AllOfBuilder::new() + .item(#items_tokens) + .item( + utoipa::openapi::schema::ObjectBuilder::new() + #title_tokens + #default_tokens + #description_stream + ) + } } else { items_tokens }; @@ -1358,7 +1368,7 @@ impl ComponentSchema { // TODO: refs support `summary` field but currently there is no such field // on schemas more over there is no way to distinct the `summary` from // `description` of the ref. Should we consider supporting the summary? - let schema = if default.is_some() || nullable || title.is_some() { + let schema = if nullable { composed_or_ref(quote_spanned! {type_path.span()=> utoipa::openapi::schema::OneOfBuilder::new() #nullable_item @@ -1374,6 +1384,8 @@ impl ComponentSchema { utoipa::openapi::schema::RefBuilder::new() #description_stream .ref_location_from_schema_name(#name_tokens) + #title_tokens + #default_tokens }) }; diff --git a/utoipa-gen/tests/schema_derive_test.rs b/utoipa-gen/tests/schema_derive_test.rs index 301681ce..2d64c749 100644 --- a/utoipa-gen/tests/schema_derive_test.rs +++ b/utoipa-gen/tests/schema_derive_test.rs @@ -3273,3 +3273,72 @@ fn test_new_type_struct_pattern() { assert_json_snapshot!(value); } + +#[test] +fn derive_option_ref_with_nullable_false() { + #[derive(ToSchema)] + #[allow(unused)] + struct RefType { + value: String, + } + + let schema = api_doc! { + struct TestStruct { + // Should generate a direct $ref without oneOf + #[schema(nullable = false)] + optional_ref: Option, + + // For comparison - default Option behavior with implicit nullable = true + default_optional_ref: Option, + } + }; + + assert_json_snapshot!(schema); +} + +#[test] +fn derive_option_ref_with_nullable_false_and_default() { + #[derive(ToSchema)] + #[allow(unused)] + struct RefType { + value: String, + } + + let schema = api_doc! { + struct TestStruct { + // Should generate a direct $ref without oneOf + #[schema(nullable = false)] + #[schema(default = json!({"value": "foo"}))] + optional_ref: Option, + + // For comparison - default Option behavior with implicit nullable = true + default_optional_ref: Option, + } + }; + + assert_json_snapshot!(schema); +} + +#[test] +fn derive_inline_option_ref_with_nullable_false_and_default() { + #[derive(ToSchema)] + #[allow(unused)] + struct RefType { + value: String, + } + + let schema = api_doc! { + struct TestStruct { + // Should generate a direct object without oneOf + #[schema(nullable = false)] + #[schema(default = json!({"value": "foo"}))] + #[schema(inline = true)] + optional_ref: Option, + + // For comparison - default Option behavior with implicit nullable = true + default_optional_ref: Option, + } + }; + + assert_json_snapshot!(schema); +} diff --git a/utoipa-gen/tests/snapshots/schema_derive_test__derive_inline_option_ref_with_nullable_false_and_default.snap b/utoipa-gen/tests/snapshots/schema_derive_test__derive_inline_option_ref_with_nullable_false_and_default.snap new file mode 100644 index 00000000..1bc972b8 --- /dev/null +++ b/utoipa-gen/tests/snapshots/schema_derive_test__derive_inline_option_ref_with_nullable_false_and_default.snap @@ -0,0 +1,40 @@ +--- +source: utoipa-gen/tests/schema_derive_test.rs +expression: schema +--- +{ + "properties": { + "default_optional_ref": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RefType" + } + ] + }, + "optional_ref": { + "allOf": [ + { + "properties": { + "value": { + "type": "string" + } + }, + "required": [ + "value" + ], + "type": "object" + }, + { + "default": { + "value": "foo" + }, + "type": "object" + } + ] + } + }, + "type": "object" +} diff --git a/utoipa-gen/tests/snapshots/schema_derive_test__derive_option_ref_with_nullable_false.snap b/utoipa-gen/tests/snapshots/schema_derive_test__derive_option_ref_with_nullable_false.snap new file mode 100644 index 00000000..91bf0e9c --- /dev/null +++ b/utoipa-gen/tests/snapshots/schema_derive_test__derive_option_ref_with_nullable_false.snap @@ -0,0 +1,22 @@ +--- +source: utoipa-gen/tests/schema_derive_test.rs +expression: schema +--- +{ + "properties": { + "default_optional_ref": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RefType" + } + ] + }, + "optional_ref": { + "$ref": "#/components/schemas/RefType" + } + }, + "type": "object" +} diff --git a/utoipa-gen/tests/snapshots/schema_derive_test__derive_option_ref_with_nullable_false_and_default.snap b/utoipa-gen/tests/snapshots/schema_derive_test__derive_option_ref_with_nullable_false_and_default.snap new file mode 100644 index 00000000..1cdbb207 --- /dev/null +++ b/utoipa-gen/tests/snapshots/schema_derive_test__derive_option_ref_with_nullable_false_and_default.snap @@ -0,0 +1,25 @@ +--- +source: utoipa-gen/tests/schema_derive_test.rs +expression: schema +--- +{ + "properties": { + "default_optional_ref": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RefType" + } + ] + }, + "optional_ref": { + "$ref": "#/components/schemas/RefType", + "default": { + "value": "foo" + } + } + }, + "type": "object" +} diff --git a/utoipa-gen/tests/snapshots/schema_derive_test__derive_schema_unnamed_title-2.snap b/utoipa-gen/tests/snapshots/schema_derive_test__derive_schema_unnamed_title-2.snap index 2f057c4b..03bffc49 100644 --- a/utoipa-gen/tests/snapshots/schema_derive_test__derive_schema_unnamed_title-2.snap +++ b/utoipa-gen/tests/snapshots/schema_derive_test__derive_schema_unnamed_title-2.snap @@ -4,10 +4,6 @@ expression: enum_value snapshot_kind: text --- { - "oneOf": [ - { - "$ref": "#/components/schemas/UnnamedEnum" - } - ], + "$ref": "#/components/schemas/UnnamedEnum", "title": "This is enum ref title" } diff --git a/utoipa-gen/tests/snapshots/schema_derive_test__derive_struct_inline_with_description.snap b/utoipa-gen/tests/snapshots/schema_derive_test__derive_struct_inline_with_description.snap index 83aa9b6c..78f4d3d1 100644 --- a/utoipa-gen/tests/snapshots/schema_derive_test__derive_struct_inline_with_description.snap +++ b/utoipa-gen/tests/snapshots/schema_derive_test__derive_struct_inline_with_description.snap @@ -1,7 +1,6 @@ --- source: utoipa-gen/tests/schema_derive_test.rs expression: "&value" -snapshot_kind: text --- { "properties": { @@ -17,8 +16,7 @@ snapshot_kind: text "type": "object" }, "with_description": { - "description": "This is description", - "oneOf": [ + "allOf": [ { "properties": { "name": { @@ -29,6 +27,10 @@ snapshot_kind: text "name" ], "type": "object" + }, + { + "description": "This is description", + "type": "object" } ] } diff --git a/utoipa-gen/tests/snapshots/schema_derive_test__derive_struct_with_default_attr_field.snap b/utoipa-gen/tests/snapshots/schema_derive_test__derive_struct_with_default_attr_field.snap index 7a0989a7..f95823e8 100644 --- a/utoipa-gen/tests/snapshots/schema_derive_test__derive_struct_with_default_attr_field.snap +++ b/utoipa-gen/tests/snapshots/schema_derive_test__derive_struct_with_default_attr_field.snap @@ -30,14 +30,10 @@ snapshot_kind: text "type": "array" }, "favorite_book": { + "$ref": "#/components/schemas/Book", "default": { "name": "Dune" - }, - "oneOf": [ - { - "$ref": "#/components/schemas/Book" - } - ] + } }, "leases": { "additionalProperties": { diff --git a/utoipa/src/openapi/schema.rs b/utoipa/src/openapi/schema.rs index 859fb46c..2e42ad4d 100644 --- a/utoipa/src/openapi/schema.rs +++ b/utoipa/src/openapi/schema.rs @@ -1397,6 +1397,14 @@ builder! { /// referenced component does not support summary field this does not have effect. #[serde(skip_serializing_if = "String::is_empty", default)] pub summary: String, + + /// A default value which by default should override that of the referenced component. + #[serde(skip_serializing_if = "Option::is_none")] + pub default: Option, + + /// A title which by default should override that of the referenced component.. + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, } } @@ -1451,6 +1459,16 @@ impl RefBuilder { pub fn summary>(mut self, summary: S) -> Self { set_value!(self summary summary.into()) } + + /// Add or change default value for the object which by default should override that of the referenced component. + pub fn default(mut self, default: Option) -> Self { + set_value!(self default default) + } + + /// Add or change the title for the object which by default should override that of the referenced component. + pub fn title>(mut self, title: Option) -> Self { + set_value!(self title title.map(|title| title.into())) + } } impl From for RefOr {