-
-
Notifications
You must be signed in to change notification settings - Fork 294
Description
I was having trouble getting named variants in code generated from my schema, and found the _unstable_ref_variants which fixed it (in combination with a transform I added to add a discriminator for every enum (see below). But, it I'm somewhat concerned that it uses the variant for the name -- it seems like this should be an option, or it should default to the Enum + Variant as a name. The rationale being it could be common to have an Error and Success variant in a variety of enums. If the name is just the variant, then we get a collision and an invalid schema.
enum A {
MyCase { ... }
}
enum B {
MyCase { ... }
}
It is still ok for serde to use MyCase as the tag in both cases, but the generated schema needs a unique name like A/MyCase or AMyCase.
In terms of adding the discriminators, I'm using something like the following right now:
/// A schemars [`Transform`](schemars::transform::Transform) that adds an OpenAPI
/// `discriminator` object to `oneOf` schemas generated from `#[serde(tag = "...")]` enums.
///
/// # Usage
///
/// ```ignore
/// #[derive(schemars::JsonSchema, serde::Serialize, serde::Deserialize)]
/// #[serde(tag = "type", rename_all = "camelCase")]
/// #[schemars(transform = AddDiscriminator::new("type"))]
/// enum MyEnum {
/// #[schemars(title = "VariantA")]
/// VariantA,
/// #[schemars(title = "VariantB")]
/// VariantB { value: String },
/// }
/// ```
pub struct AddDiscriminator {
property_name: String,
}
impl AddDiscriminator {
/// Create a new `AddDiscriminator` transform for the given tag property name.
///
/// The `property_name` should match the `tag` value in `#[serde(tag = "...")]`.
pub fn new(property_name: impl Into<String>) -> Self {
Self {
property_name: property_name.into(),
}
}
/// Extract the discriminator tag's `const` value from a oneOf variant schema.
///
/// Handles both inline schemas (direct `properties`) and `allOf` patterns
/// (where a `$ref` is combined with discriminator properties).
fn find_tag_const<'a>(variant: &'a Value, tag_property: &str) -> Option<&'a str> {
// Check direct properties (inline variant)
if let Some(val) = variant
.get("properties")
.and_then(|p| p.get(tag_property))
.and_then(|p| p.get("const"))
.and_then(|c| c.as_str())
{
return Some(val);
}
// Check allOf items
if let Some(all_of) = variant.get("allOf").and_then(|v| v.as_array()) {
for item in all_of {
if let Some(val) = item
.get("properties")
.and_then(|p| p.get(tag_property))
.and_then(|p| p.get("const"))
.and_then(|c| c.as_str())
{
return Some(val);
}
}
}
None
}
/// Extract a `$ref` path from a oneOf variant schema.
fn find_ref(variant: &Value) -> Option<&str> {
if let Some(ref_str) = variant.get("$ref").and_then(|r| r.as_str()) {
return Some(ref_str);
}
if let Some(all_of) = variant.get("allOf").and_then(|v| v.as_array()) {
for item in all_of {
if let Some(ref_str) = item.get("$ref").and_then(|r| r.as_str()) {
return Some(ref_str);
}
}
}
None
}
}
impl schemars::transform::Transform for AddDiscriminator {
fn transform(&mut self, schema: &mut schemars::Schema) {
let Some(obj) = schema.as_object_mut() else {
return;
};
let Some(one_of) = obj.get("oneOf").and_then(|v| v.as_array()) else {
return;
};
// Build mapping from discriminator values to $ref paths
let mut mapping = Map::new();
for variant in one_of {
if let Some(tag_value) = Self::find_tag_const(variant, &self.property_name)
&& let Some(ref_path) = Self::find_ref(variant)
{
mapping.insert(tag_value.to_string(), Value::String(ref_path.to_string()));
}
}
// Build the discriminator object
let mut discriminator = Map::new();
discriminator.insert(
"propertyName".to_string(),
Value::String(self.property_name.clone()),
);
if !mapping.is_empty() {
discriminator.insert("mapping".to_string(), Value::Object(mapping));
}
obj.insert("discriminator".to_string(), Value::Object(discriminator));
}
}
(with some tests to make sure it works as expected). I guess I could modify my transform to also rename all of the extracted variants, but that seems a bit excessive. I'm wondering if there is a case for going the other way -- supporting add the discriminator as an option #[schemars(discriminator)] and adding a #[schemars(variant_def = "name")] option to variants (and/or defaulting to including the enum name in the variant definition name).