-
Notifications
You must be signed in to change notification settings - Fork 80
A note on polymorphism and discriminators. #116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
This was referenced Feb 15, 2024
I just tried openapicmd for your case 1 and here is what I got: npx openapicmd typegen http://localhost:3001/api/docs/private/api.json > ./src/openapi.d.ts ...
declare namespace Components {
namespace Schemas {
export type AnyItem = {
table: TableItem;
} | {
chart: ChartItem;
};
export interface ChartItem {
image_path: string;
title?: string | null;
}
export interface TableItem {
colnames: string;
rows: string;
title?: string | null;
}
}
}
declare namespace Paths {
namespace ApiTestFn {
namespace Get {
namespace Responses {
export type $200 = Components.Schemas.AnyItem;
}
}
}
}
... So for type safety in Typescript, you have to check if you got an |
This worked for me out great #[derive(Deserialize, Serialize, Debug)]
#[serde(tag = "item_type", rename_all = "lowercase")]
pub enum AnyItem {
Table(TableItem),
Chart(ChartItem),
}
pub fn add_discriminator_case(tag: &str, value: &str) -> schemars::schema::Schema {
schemars::schema::Schema::Object(schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::Object.into()),
object: Some(Box::new(schemars::schema::ObjectValidation {
properties: {
let mut props = schemars::Map::new();
props.insert(
tag.to_owned(),
schemars::schema::Schema::Object(schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
enum_values: Some(<[_]>::into_vec(Box::new([value.to_owned().into()]))),
..Default::default()
}),
);
props
},
required: {
let mut required = schemars::Set::new();
required.insert(tag.to_owned());
required
},
..Default::default()
})),
..Default::default()
})
}
impl JsonSchema for AnyItem {
fn schema_name() -> String {
"AnyItem".to_string()
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
let discriminator = json!({
"propertyName": "item_type",
});
let table_type = add_discriminator_case("item_type", "table");
let chart_type = add_discriminator_case("item_type", "chart");
let subschemas = schemars::schema::SubschemaValidation {
one_of: Some(vec![
table_type.flatten(<TableItem as schemars::JsonSchema>::json_schema(gen)),
chart_type.flatten(<ChartItem as schemars::JsonSchema>::json_schema(gen)),
]),
..Default::default()
};
let schema_object = schemars::schema::SchemaObject {
subschemas: Some(Box::new(subschemas)),
extensions: BTreeMap::from_iter(vec![("discriminator".to_owned(), discriminator)]),
..Default::default()
};
schemars::schema::Schema::Object(schema_object)
}
} for generating the typescript schema, I did use the final result ending looking like this, AnyItem: ({item_type: "table", ...rest}) | ({item_type: "chart", ...rest}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is not really an issue. I'm writing it down for ones who come after us. I've spent unreasonable amount of time on this but I believe it is worth it. Rust is great for backends, Axum is likely the best option for the http stack and
aide
is the only option to generate OpenAPI automatically I managed to get usable results with. This was the only long standing issue for me as I rely heavily on the code generation from the OpenAPI spec. I'd suggest that the final solution should be at least mentioned in the official documentation as others will likely experience the same issue.I have a simulated polymorphism where the classes
TableItem
,ChartItem
, ... can be stored inside a container calledAnyItem
. My implementation is similar to thisThe OpenAPI JSON file is consumed by datamodel-code-generator to generate Python's Pydantic classes. And it is also consumed by openapi-generator to generate Typescript models using the typescript-fetch generator. Both of those had a problem with
AnyItem
.I'm doing serialization in Python and deserialization in Typescript so this is the only case I'll cover here.
Existing behavior
Case 1: Basic enum
Rust definition
OpenAPI
Problem is that both generators generate structs
AnyItem1
,AnyItem2
for each case of theoneOf
. This is major PITA when trying to create the data structures as one has to guess which number corresponds to the type. Deserialization is fine.Case 2: Tagged enum
Rust definition
OpenAPI
This one has a problem that the actual structures
ChartItem
, ... don't have theitem_type
field so it cannot use references. It correctly inlined the temporary objects here. This situation is also bad for both serialization and deserialization as those inlined objects are completely different from the real ones.Case 3: Tag and content
Rust code
OpenAPI
This fixes the problem on inlined objects from case 2 but it also creates temporary objects as was the case in 1.
Case 4: Untagged enum
Rust code
OpenAPI
This one is so close. The only problem is that there is no field marking which objects we are actually holding and so deserialization can get a little bit tricky. My problem was that the Typescript codegen created a superset object for
AnyItem
containing all fields from the items and that made it impossible to distinguish each object just by looking at fields.Final solution
The solution I arrived at was to create a
oneOf
withdiscriminator
andmapping
as described in this blog post. I tried that manually and both generators produced a usable code without any temporary objects. We also need to use a tagged enums. How it works is that Serde adds a field with the type information. I hide this fact from the schema ofAnyItem
and instead use adiscriminator
. Here is my code.This generates following OpenAPI specification
In typescript, type of the
AnyItem
isGood thing is that the
itemType
field is added on top andChartItem
andTableItem
are my exact objects. Generated code also usesitemType
in a switch to decide how to properly deserialize the objects.In Python the type of
AnyItem
isWhich is also good. The only slight inconvenience is that it also added the
item_type
field toTableItem
andChartItem
so I have to provide it when creating the object.The text was updated successfully, but these errors were encountered: