-
Notifications
You must be signed in to change notification settings - Fork 74
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
top-level types are always inline, so duplicated in the schema #104
Comments
With this change: diff --git a/dropshot/src/handler.rs b/dropshot/src/handler.rs
index 85d544d..38560c2 100644
--- a/dropshot/src/handler.rs
+++ b/dropshot/src/handler.rs
@@ -1163,13 +1163,20 @@ where
ApiEndpointResponse {
schema: Some(ApiSchemaGenerator::Gen {
name: T::Body::schema_name,
- schema: T::Body::json_schema,
+ schema: make_subschema_for::<T::Body>,
}),
success: Some(T::STATUS_CODE),
description: Some(T::DESCRIPTION.to_string()),
}
}
}
+
+fn make_subschema_for<T: JsonSchema>(
+ gen: &mut schemars::gen::SchemaGenerator,
+) -> schemars::schema::Schema {
+ gen.subschema_for::<T>()
+}
+ With this change, this is how the schema for test_openapi.rs changes: dap@zathras dropshot $ git diff dropshot/tests/test_openapi.json
diff --git a/dropshot/tests/test_openapi.json b/dropshot/tests/test_openapi.json
index 1233f79..4ce534a 100644
--- a/dropshot/tests/test_openapi.json
+++ b/dropshot/tests/test_openapi.json
@@ -26,6 +26,40 @@
}
}
},
+ "/dup1": {
+ "put": {
+ "operationId": "handler8",
+ "responses": {
+ "200": {
+ "description": "successful operation",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NeverDuplicatedTopLevel"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/dup2": {
+ "put": {
+ "operationId": "handler9",
+ "responses": {
+ "200": {
+ "description": "successful operation",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NeverDuplicatedTopLevel"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/impairment": {
"get": {
"operationId": "handler6",
@@ -67,25 +101,7 @@
"content": {
"application/json": {
"schema": {
- "title": "ResponseItemResultsPage",
- "description": "A single page of results",
- "type": "object",
- "properties": {
- "items": {
- "description": "list of items on this page of results",
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/ResponseItem"
- }
- },
- "next_page": {
- "description": "token used to fetch the next page of results (if any)",
- "type": "string"
- }
- },
- "required": [
- "items"
- ]
+ "$ref": "#/components/schemas/ResponseItemResultsPage"
}
}
}
@@ -122,8 +138,7 @@
"content": {
"application/json": {
"schema": {
- "title": "Response",
- "type": "object"
+ "$ref": "#/components/schemas/Response"
}
}
}
@@ -288,6 +303,31 @@
},
"components": {
"schemas": {
+ "NeverDuplicatedNextLevel": {
+ "type": "object",
+ "properties": {
+ "v": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "v"
+ ]
+ },
+ "NeverDuplicatedTopLevel": {
+ "type": "object",
+ "properties": {
+ "b": {
+ "$ref": "#/components/schemas/NeverDuplicatedNextLevel"
+ }
+ },
+ "required": [
+ "b"
+ ]
+ },
+ "Response": {
+ "type": "object"
+ },
"ResponseItem": {
"type": "object",
"properties": {
@@ -298,6 +338,26 @@
"required": [
"word"
]
+ },
+ "ResponseItemResultsPage": {
+ "description": "A single page of results",
+ "type": "object",
+ "properties": {
+ "items": {
+ "description": "list of items on this page of results",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ResponseItem"
+ }
+ },
+ "next_page": {
+ "description": "token used to fetch the next page of results (if any)",
+ "type": "string"
+ }
+ },
+ "required": [
+ "items"
+ ]
}
}
} The hunks related to ResponseItemResultsPage are noise -- those are other instances of the same problem being fixed. Importantly, we see the top-level type showing up as a reference now, with the body showing up in This is promising but I'm not positive I've really applied the fix at the right spot. That said, I'm not sure where else it would go. There are also a few other callers of |
Query parameters seem to have the same problem. I confirmed with this change: diff --git a/dropshot/tests/test_openapi.rs b/dropshot/tests/test_openapi.rs
index 3af7e5f..621ce4f 100644
--- a/dropshot/tests/test_openapi.rs
+++ b/dropshot/tests/test_openapi.rs
@@ -147,7 +147,7 @@ struct NeverDuplicatedNextLevel {
}
#[endpoint {
- method = PUT,
+ method = GET,
path = "/dup1",
}]
async fn handler8(
@@ -157,7 +157,7 @@ async fn handler8(
}
#[endpoint {
- method = PUT,
+ method = GET,
path = "/dup2",
}]
async fn handler9(
@@ -166,6 +166,38 @@ async fn handler9(
unimplemented!();
}
+#[derive(Deserialize, JsonSchema)]
+struct NeverDuplicatedParamTopLevel {
+ _b: NeverDuplicatedParamNextLevel
+}
+
+#[derive(Deserialize, JsonSchema)]
+struct NeverDuplicatedParamNextLevel {
+ _v: bool
+}
+
+#[endpoint {
+ method = PUT,
+ path = "/dup3",
+}]
+async fn handler10(
+ _rqctx: Arc<RequestContext<()>>,
+ _q: Query<NeverDuplicatedParamTopLevel>,
+) -> Result<HttpResponseOk<()>, HttpError> {
+ unimplemented!();
+}
+
+#[endpoint {
+ method = PUT,
+ path = "/dup4",
+}]
+async fn handler11(
+ _rqctx: Arc<RequestContext<()>>,
+ _q: Query<NeverDuplicatedParamTopLevel>,
+) -> Result<HttpResponseOk<()>, HttpError> {
+ unimplemented!();
+}
+
fn make_api() -> Result<ApiDescription<()>, String> {
let mut api = ApiDescription::new();
api.register(handler1)?;
@@ -177,6 +209,8 @@ fn make_api() -> Result<ApiDescription<()>, String> {
api.register(handler7)?;
api.register(handler8)?;
api.register(handler9)?;
+ api.register(handler10)?;
+ api.register(handler11)?;
Ok(api)
}
We get this change in spec: diff --git a/dropshot/tests/test_openapi_fuller.json b/dropshot/tests/test_openapi_fuller.json
index 19a1c59..e602b4a 100644
--- a/dropshot/tests/test_openapi_fuller.json
+++ b/dropshot/tests/test_openapi_fuller.json
@@ -35,7 +35,7 @@
}
},
"/dup1": {
- "put": {
+ "get": {
"operationId": "handler8",
"responses": {
"200": {
@@ -52,7 +52,7 @@
}
},
"/dup2": {
- "put": {
+ "get": {
"operationId": "handler9",
"responses": {
"200": {
@@ -68,6 +68,48 @@
}
}
},
+ "/dup3": {
+ "put": {
+ "operationId": "handler10",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "_b",
+ "required": true,
+ "schema": {
+ "$ref": "#/components/schemas/NeverDuplicatedParamNextLevel"
+ },
+ "style": "form"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "successful operation"
+ }
+ }
+ }
+ },
+ "/dup4": {
+ "put": {
+ "operationId": "handler11",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "_b",
+ "required": true,
+ "schema": {
+ "$ref": "#/components/schemas/NeverDuplicatedParamNextLevel"
+ },
+ "style": "form"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "successful operation"
+ }
+ }
+ }
+ },
"/impairment": {
"get": {
"operationId": "handler6",
@@ -366,6 +408,17 @@
"required": [
"items"
]
+ },
+ "NeverDuplicatedParamNextLevel": {
+ "type": "object",
+ "properties": {
+ "_v": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "_v"
+ ]
}
}
} |
I think this probably also affects TypedBody as well. The query params case is a little trickier to fix than I expected because the obvious change breaks the way we're detecting whether this is a paginated endpoint. |
I think I misread the output from the query params case in my comment above. I realized this when I took a closer look at the generated schema. The spec sort of looks like it has inlined the top-level type, in that there are two schemas for an object with property |
The problem
In oxidecomputer/omicron#64 @david-crespo reported that when running openapi-generator for TypeScript on the Omicron API schema, he was getting duplicate type definitions like
ApiRackView
andApiRackView1
. The types were exactly the same. This API has two different endpoints that use this type: one returns anApiRackView
, and the other returns a list ofApiRackView
. It appears that the dropshot-generated OpenAPI spec inlines the top-level type in the response, but it generates references for subtypes.Test case
I was able to replicate this by modifying test_openapi.rs with this change:
Here's how that changes the generated schema for that test:
We see that the top-level type shows up twice, inline in both places, while the next level type gets a reference instead.
Analysis
When you use the
endpoint
macro, we generate aFrom<...> for ApiEndpoint
for this specific handler(That's from
cargo expand -p dropshot --test=test_openapi
with my changes.)When you register the handler function, we use that
From
impl (well, the correspondingInto
) here:dropshot/dropshot/src/api_description.rs
Lines 222 to 226 in e04241c
We see above that that invokes
ApiEndpoint::new()
, which winds up invokingmetadata()
on the response type:dropshot/dropshot/src/api_description.rs
Line 60 in e04241c
ResponseType
isHttpResponseOk
here, and themetadata()
impl comes fromHttpTypedResponse
:dropshot/dropshot/src/handler.rs
Lines 1140 to 1149 in e04241c
What's important here is that we supply
Body::json_schema
as theschema
function here:dropshot/dropshot/src/handler.rs
Line 1144 in e04241c
Again, that's all at registration time. Later when we generate the schema, we use that
schema
function to generate a schema for the response body:dropshot/dropshot/src/api_description.rs
Lines 524 to 527 in e04241c
Let's take a closer look at the derived
JsonSchema
impls. For the top-level type, it looks like this (back tocargo expand
):The schema for
NeverDuplicatedNextLevel
must be added viaadd_schema_as_property
, which winds up invoking notjson_schema()
onNeverDuplicatedNextLevel
, but gen.subschema_for(). That's documented as:This explains why the non-top-level schemas get references: JsonSchema internally uses a function that tries to add references if possible. The top-level ones don't get references because we're using
json_schema()
directly, which always generates a new, non-reference schema.The text was updated successfully, but these errors were encountered: