Hi,
we are currently designing an API that can return one of multiple different structs from an endpoint.
The structs contain some shared fields, so we want to use a discriminator field so that the generated REST clients can clearly distinguish between the different types.
We also want to enforce type safety as much as possible in our rust code as well as for generated REST clients, so we want to avoid optional fields and use unions/enums instead.
However, we noticed that it is a bit difficult at the moment to use utoipa together with #[serde(tag ...)] because it creates new, unnamed objects for each enum variant, which are very tedious to use on client side. Also, the #[schema(discriminator... )] macro can only be used for untagged enums.
We first tried the following apporach:
#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)]
pub struct OperationStateCompleted {
#[schema(value_type = String, format = "date-time")]
pub created_at: String,
#[schema(value_type = String, format = "date-time")]
pub completed_at: String,
}
#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)]
pub struct OperationStatePending {
#[schema(value_type = String, format = "date-time")]
pub created_at: String,
}
/// Represents the entire state of a long-running operation.
#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)]
#[serde(tag = "status")]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum OperationState {
Pending(OperationStatePending),
Completed(OperationStateCompleted),
}
This works fine in Rust and serde serializes/deserializes exactly what we want, but Utoipa generates the following Schema:
OperationState:
oneOf:
- allOf:
- $ref: '#/components/schemas/OperationStatePending'
- type: object
required:
- status
properties:
status:
type: string
enum:
- PENDING
- allOf:
- $ref: '#/components/schemas/OperationStateCompleted'
- type: object
required:
- status
properties:
status:
type: string
enum:
- COMPLETED
Even though serde serializes the status tag into the enum variants directly, Utoipa creates a new, anonymous object via allOf for each enum variant. This results in unusable generated clients, because instead of generating a union of (OperationStatePending | OperationStateCompleted) clients will now generate a union of (OperationStateOneOf | OperationStateOneOf1) or similar, with nested types inside.
Instead, we think that Utoipa should automatically recognize the serde(tag) and treat this field as discriminator for OpenAPI, so it should generate something like:
OperationState:
oneOf:
- $ref: '#/components/schemas/OperationStatePending'
- $ref: '#/components/schemas/OperationStateCompleted'
discriminator:
propertyName: status
This would also have to include the status member for the OperationState schemas and may also use the advanced dsicriminator syntax that maps between property value and Schema.
To solve the issue, we now had to create some suboptimal rust enums and structs to manually build the desired behavior:
#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum OperationStateCompletedDiscriminator {
Completed,
}
#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum OperationStatePendingDiscriminator {
Pending,
}
#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)]
pub struct OperationStateCompleted {
#[schema(inline)]
pub status: OperationStateCompletedDiscriminator,
#[schema(value_type = String, format = "date-time")]
pub created_at: String,
#[schema(value_type = String, format = "date-time")]
pub completed_at: String,
}
#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)]
pub struct OperationStatePending {
#[schema(inline)]
pub status: OperationStatePendingDiscriminator,
#[schema(value_type = String, format = "date-time")]
pub created_at: String,
}
/// Represents the entire state of a long-running operation.
#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)]
#[serde(untagged)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[schema(discriminator = "status")]
pub enum OperationState {
Pending(OperationStatePending),
Completed(OperationStateCompleted),
}
While this works, this makes the rust code very verbose and error prone. We also experimented with ideas like manually implementing ToSchema or custom derive macros, but all of these approaches only resulted in even more complexity and sometimes removed compile time guarantees.
I am not sure if we are missing a special combination of options/macros to make this possible. If not, we think this would be a great addition to Utoipa for increased type safety and comfort when using discriminators and oneOf types.
Hi,
we are currently designing an API that can return one of multiple different structs from an endpoint.
The structs contain some shared fields, so we want to use a discriminator field so that the generated REST clients can clearly distinguish between the different types.
We also want to enforce type safety as much as possible in our rust code as well as for generated REST clients, so we want to avoid optional fields and use unions/enums instead.
However, we noticed that it is a bit difficult at the moment to use utoipa together with #[serde(tag ...)] because it creates new, unnamed objects for each enum variant, which are very tedious to use on client side. Also, the #[schema(discriminator... )] macro can only be used for untagged enums.
We first tried the following apporach:
This works fine in Rust and serde serializes/deserializes exactly what we want, but Utoipa generates the following Schema:
Even though serde serializes the status tag into the enum variants directly, Utoipa creates a new, anonymous object via allOf for each enum variant. This results in unusable generated clients, because instead of generating a union of (OperationStatePending | OperationStateCompleted) clients will now generate a union of (OperationStateOneOf | OperationStateOneOf1) or similar, with nested types inside.
Instead, we think that Utoipa should automatically recognize the serde(tag) and treat this field as discriminator for OpenAPI, so it should generate something like:
This would also have to include the status member for the OperationState schemas and may also use the advanced dsicriminator syntax that maps between property value and Schema.
To solve the issue, we now had to create some suboptimal rust enums and structs to manually build the desired behavior:
While this works, this makes the rust code very verbose and error prone. We also experimented with ideas like manually implementing ToSchema or custom derive macros, but all of these approaches only resulted in even more complexity and sometimes removed compile time guarantees.
I am not sure if we are missing a special combination of options/macros to make this possible. If not, we think this would be a great addition to Utoipa for increased type safety and comfort when using discriminators and oneOf types.