Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions utoipa-gen/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog - utoipa-gen

## Unreleased

### Added

* Add support for `#[schema(title_variants)]` on mixed enums (https://github.com/juhaku/utoipa/pull/1511)

## 5.4.0 - Jun 16 2025

### Added
Expand Down
6 changes: 6 additions & 0 deletions utoipa-gen/src/component/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ pub enum Feature {
WriteOnly(attributes::WriteOnly),
ReadOnly(attributes::ReadOnly),
Title(attributes::Title),
TitleVariants(attributes::TitleVariants),
Nullable(attributes::Nullable),
Rename(attributes::Rename),
RenameAll(attributes::RenameAll),
Expand Down Expand Up @@ -179,6 +180,7 @@ impl ToTokensDiagnostics for Feature {
Feature::WriteOnly(write_only) => quote! { .write_only(Some(#write_only)) },
Feature::ReadOnly(read_only) => quote! { .read_only(Some(#read_only)) },
Feature::Title(title) => quote! { .title(Some(#title)) },
Feature::TitleVariants(_) => return Err(Diagnostics::new("TitleVariants does not support `ToTokens`")),
Feature::Nullable(_nullable) => return Err(Diagnostics::new("Nullable does not support `ToTokens`")),
Feature::Rename(rename) => rename.to_token_stream(),
Feature::Style(style) => quote! { .style(Some(#style)) },
Expand Down Expand Up @@ -274,6 +276,7 @@ impl Display for Feature {
Feature::WriteOnly(write_only) => write_only.fmt(f),
Feature::ReadOnly(read_only) => read_only.fmt(f),
Feature::Title(title) => title.fmt(f),
Feature::TitleVariants(title_variants) => title_variants.fmt(f),
Feature::Nullable(nullable) => nullable.fmt(f),
Feature::Rename(rename) => rename.fmt(f),
Feature::Style(style) => style.fmt(f),
Expand Down Expand Up @@ -324,6 +327,7 @@ impl Validatable for Feature {
Feature::WriteOnly(write_only) => write_only.is_validatable(),
Feature::ReadOnly(read_only) => read_only.is_validatable(),
Feature::Title(title) => title.is_validatable(),
Feature::TitleVariants(title_variants) => title_variants.is_validatable(),
Feature::Nullable(nullable) => nullable.is_validatable(),
Feature::Rename(rename) => rename.is_validatable(),
Feature::Style(style) => style.is_validatable(),
Expand Down Expand Up @@ -388,6 +392,7 @@ is_validatable! {
attributes::WriteOnly,
attributes::ReadOnly,
attributes::Title,
attributes::TitleVariants,
attributes::Nullable,
attributes::Rename,
attributes::RenameAll,
Expand Down Expand Up @@ -623,6 +628,7 @@ impl_feature_into_inner! {
attributes::WriteOnly,
attributes::ReadOnly,
attributes::Title,
attributes::TitleVariants,
attributes::Nullable,
attributes::Rename,
attributes::RenameAll,
Expand Down
23 changes: 22 additions & 1 deletion utoipa-gen/src/component/features/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,27 @@ impl From<Examples> for Feature {
}
}

impl_feature! {
#[derive(Clone)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct TitleVariants;
}

impl Parse for TitleVariants {
fn parse(_: ParseStream, _: Ident) -> syn::Result<Self>
where
Self: std::marker::Sized,
{
Ok(Self)
}
}

impl From<TitleVariants> for Feature {
fn from(value: TitleVariants) -> Self {
Feature::TitleVariants(value)
}
}

impl_feature! {"xml" =>
#[derive(Default, Clone)]
#[cfg_attr(feature = "debug", derive(Debug))]
Expand Down Expand Up @@ -258,7 +279,7 @@ impl From<ReadOnly> for Feature {
impl_feature! {
#[derive(Clone)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct Title(String);
pub struct Title(pub String);
}

impl Parse for Title {
Expand Down
23 changes: 21 additions & 2 deletions utoipa-gen/src/component/schema/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
features::{
attributes::{
Deprecated, Description, Discriminator, Example, Examples, NoRecursion, Rename,
RenameAll, Title,
RenameAll, Title, TitleVariants,
},
parse_features, pop_feature, Feature, IntoInner, IsInline, ToTokensExt,
},
Expand Down Expand Up @@ -255,6 +255,15 @@ impl<'p> MixedEnum<'p> {
let rename_all = pop_feature!(features => Feature::RenameAll(_) as Option<RenameAll>);
let description = pop_feature!(features => Feature::Description(_) as Option<Description>);
let discriminator = pop_feature!(features => Feature::Discriminator(_));
let title_variants =
pop_feature!(features => Feature::TitleVariants(_) as Option<TitleVariants>);
let variants_title_prefix = features
.iter()
.find_map(|feature| match feature {
Feature::Title(Title(title)) => Some(title.clone()),
_ => None,
})
.unwrap_or_else(|| root.ident.to_string());

let variants = variants
.iter()
Expand All @@ -268,7 +277,7 @@ impl<'p> MixedEnum<'p> {
if variant_rules.skip {
None
} else {
let variant_features = match &variant.fields {
let mut variant_features = match &variant.fields {
Fields::Named(_) => {
match variant
.attrs
Expand Down Expand Up @@ -306,6 +315,16 @@ impl<'p> MixedEnum<'p> {
}
};

if title_variants.is_some()
&& !variant_features
.iter()
.any(|f| matches!(f, Feature::Title(_)))
{
let variant_name = variant.ident.to_string();
let title = format!("{variants_title_prefix}{variant_name}");
variant_features.push(Feature::Title(Title(title)));
}

Some(Ok((variant, variant_rules, variant_features)))
}
})
Expand Down
5 changes: 3 additions & 2 deletions utoipa-gen/src/component/schema/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use crate::{
attributes::{
AdditionalProperties, As, Bound, ContentEncoding, ContentMediaType, Deprecated,
Description, Discriminator, Example, Examples, Format, Ignore, Inline, NoRecursion,
Nullable, ReadOnly, Rename, RenameAll, Required, SchemaWith, Title, ValueType,
WriteOnly, XmlAttr,
Nullable, ReadOnly, Rename, RenameAll, Required, SchemaWith, Title, TitleVariants,
ValueType, WriteOnly, XmlAttr,
},
impl_into_inner, impl_merge, parse_features,
validation::{
Expand Down Expand Up @@ -101,6 +101,7 @@ impl Parse for MixedEnumFeatures {
Examples,
crate::component::features::attributes::Default,
Title,
TitleVariants,
RenameAll,
As,
Deprecated,
Expand Down
3 changes: 3 additions & 0 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,9 @@ static CONFIG: once_cell::sync::Lazy<utoipa_config::Config> =
/// * `title = ...` Literal string value. Can be used to define title for enum in OpenAPI
/// document. Some OpenAPI code generation libraries also use this field as a name for the
/// enum.
/// * `title_variants` Can be used to generate titles for enum variants by concatenating the enum
/// title with the variant name. Variant titles can be overridden individually by annotating
/// them with the `#[schema(title = "...")]` attribute.
/// * `rename_all = ...` Supports same syntax as _serde_ _`rename_all`_ attribute. Will rename all
/// variants of the enum accordingly. If both _serde_ `rename_all` and _schema_ _`rename_all`_
/// are defined __serde__ will take precedence.
Expand Down
38 changes: 38 additions & 0 deletions utoipa-gen/tests/schema_derive_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,44 @@ fn derive_mixed_enum_title() {
assert_json_snapshot!(value);
}

#[test]
fn derive_mixed_enum_title_variants() {
#[derive(ToSchema)]
struct Foo;

let value: Value = api_doc! {
#[schema(title_variants)]
enum EnumTitleVariants {
UnitValue,
NamedFields {
id: &'static str,
},
UnnamedFields(Foo),
#[schema(title = "Overridden")]
OverriddenTitle,
}
};

assert_json_snapshot!(value);
}

#[test]
fn derive_mixed_enum_title_variants_enum_title() {
let value: Value = api_doc! {
#[schema(title = "CustomTitle", title_variants)]
enum EnumTitleVariants {
UnitValue,
NamedFields {
id: &'static str,
},
#[schema(title = "Overridden")]
OverriddenTitle,
}
};

assert_json_snapshot!(value);
}

#[test]
fn derive_mixed_enum_example() {
#[derive(Serialize, ToSchema)]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
source: utoipa-gen/tests/schema_derive_test.rs
expression: value
---
{
"oneOf": [
{
"enum": [
"UnitValue"
],
"title": "EnumTitleVariantsUnitValue",
"type": "string"
},
{
"properties": {
"NamedFields": {
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
}
},
"required": [
"NamedFields"
],
"title": "EnumTitleVariantsNamedFields",
"type": "object"
},
{
"properties": {
"UnnamedFields": {
"$ref": "#/components/schemas/Foo"
}
},
"required": [
"UnnamedFields"
],
"title": "EnumTitleVariantsUnnamedFields",
"type": "object"
},
{
"enum": [
"OverriddenTitle"
],
"title": "Overridden",
"type": "string"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
source: utoipa-gen/tests/schema_derive_test.rs
expression: value
---
{
"oneOf": [
{
"enum": [
"UnitValue"
],
"title": "CustomTitleUnitValue",
"type": "string"
},
{
"properties": {
"NamedFields": {
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
}
},
"required": [
"NamedFields"
],
"title": "CustomTitleNamedFields",
"type": "object"
},
{
"enum": [
"OverriddenTitle"
],
"title": "Overridden",
"type": "string"
}
],
"title": "CustomTitle"
}