From 77ccaca14dcc385abdfef60bf4b3696f16cfa9f6 Mon Sep 17 00:00:00 2001 From: Kiel Ed Date: Wed, 18 Mar 2026 13:27:56 +0300 Subject: [PATCH] Add multipart/form-data docs and example Add "Multipart/form-data" section to axum module docs pointing to aide-axum-typed-multipart-2 and the manual OperationInput approach. Add example-axum-multipart demonstrating typed multipart/form-data with a manual OperationInput wrapper that can be replaced with aide-axum-typed-multipart-2 once it's updated to aide 0.16. --- crates/aide/src/axum/mod.rs | 15 +++++ examples/example-axum-multipart/Cargo.toml | 18 +++++ examples/example-axum-multipart/src/main.rs | 65 +++++++++++++++++++ .../example-axum-multipart/src/multipart.rs | 57 ++++++++++++++++ 4 files changed, 155 insertions(+) create mode 100644 examples/example-axum-multipart/Cargo.toml create mode 100644 examples/example-axum-multipart/src/main.rs create mode 100644 examples/example-axum-multipart/src/multipart.rs diff --git a/crates/aide/src/axum/mod.rs b/crates/aide/src/axum/mod.rs index 2fbb61f4..d1a5d5e6 100644 --- a/crates/aide/src/axum/mod.rs +++ b/crates/aide/src/axum/mod.rs @@ -162,6 +162,21 @@ //! // ... //! ``` //! +//! # Multipart/form-data +//! +//! Aide has built-in support for `axum::extract::Multipart` via the `axum-multipart` +//! feature, but it only generates a generic schema without typed fields. +//! +//! For **typed multipart/form-data** with proper per-field OpenAPI schemas, you can either: +//! +//! - Use the community-maintained [`aide-axum-typed-multipart-2`](https://crates.io/crates/aide-axum-typed-multipart-2) +//! crate, which provides drop-in replacements for `axum_typed_multipart`'s +//! `TypedMultipart` and `FieldData` that implement [`OperationInput`](crate::OperationInput) +//! and `JsonSchema` respectively. +//! +//! - Implement [`OperationInput`](crate::OperationInput) manually with a newtype wrapper. +//! See the `example-axum-multipart` example for a complete working setup. +//! //! # Composability //! //! Just like in `axum`, nesting and merging routers is possible, diff --git a/examples/example-axum-multipart/Cargo.toml b/examples/example-axum-multipart/Cargo.toml new file mode 100644 index 00000000..9c1af2ad --- /dev/null +++ b/examples/example-axum-multipart/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "example-axum-multipart" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +aide = { path = "../../crates/aide", features = [ + "axum-json", + "scalar", +] } +axum = "0.8.1" +axum_typed_multipart = "0.16" +indexmap = "2" +schemars = "1.0.4" +serde = { version = "1.0.144", features = ["derive"] } +serde_json = "1.0.85" +tokio = { version = "1.21.0", features = ["macros", "rt-multi-thread"] } diff --git a/examples/example-axum-multipart/src/main.rs b/examples/example-axum-multipart/src/main.rs new file mode 100644 index 00000000..d1951d5b --- /dev/null +++ b/examples/example-axum-multipart/src/main.rs @@ -0,0 +1,65 @@ +use std::sync::Arc; + +use aide::{ + axum::{routing::post_with, ApiRouter, IntoApiResponse}, + openapi::OpenApi, +}; +use axum::{Extension, Json}; +use axum_typed_multipart::TryFromMultipart; +use schemars::JsonSchema; +use tokio::net::TcpListener; + +// Can be replaced with `aide_axum_typed_multipart_2::TypedMultipart` once +// the crate is updated to aide 0.16 + schemars 1.x. +mod multipart; +use multipart::DocTypedMultipart; + +#[derive(TryFromMultipart, JsonSchema)] +struct UploadForm { + /// A description of the uploaded file. + description: String, + /// The file to upload (max 5 MiB). + #[form_data(limit = "5MiB")] + #[schemars(with = "Vec")] + file: axum::body::Bytes, +} + +async fn upload(DocTypedMultipart(form): DocTypedMultipart) -> impl IntoApiResponse { + Json(serde_json::json!({ + "description": form.description, + "size": form.file.len(), + })) +} + +async fn serve_api(Extension(api): Extension>) -> impl IntoApiResponse { + Json(api.as_ref().clone()) +} + +#[tokio::main] +async fn main() { + aide::generate::extract_schemas(true); + + let mut api = OpenApi::default(); + + let app = ApiRouter::new() + .api_route( + "/upload", + post_with(upload, |op| { + op.description("Upload a file with a description.") + .response_with::<200, Json, _>(|res| { + res.description("Upload result with file size") + }) + }), + ) + .route("/api.json", axum::routing::get(serve_api)) + .finish_api_with(&mut api, |api| { + api.title("Multipart Upload Example") + .summary("Demonstrates typed multipart/form-data with aide") + }) + .layer(Extension(Arc::new(api))); + + println!("Docs available at http://127.0.0.1:3000/api.json"); + + let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} diff --git a/examples/example-axum-multipart/src/multipart.rs b/examples/example-axum-multipart/src/multipart.rs new file mode 100644 index 00000000..0a00ab9b --- /dev/null +++ b/examples/example-axum-multipart/src/multipart.rs @@ -0,0 +1,57 @@ +// Manual OperationInput + FromRequest wrapper for TypedMultipart. +// +// This can be replaced with a single import once `aide-axum-typed-multipart-2` +// is updated to aide 0.16 + schemars 1.x: +// +// use aide_axum_typed_multipart_2::TypedMultipart; + +use aide::{ + openapi::{MediaType, Operation, RequestBody, SchemaObject}, + operation::set_body, + OperationInput, +}; +use axum::extract::{FromRequest, Request}; +use axum_typed_multipart::TypedMultipart; +use indexmap::IndexMap; +use schemars::JsonSchema; + +pub struct DocTypedMultipart(pub T); + +impl OperationInput for DocTypedMultipart { + fn operation_input(ctx: &mut aide::generate::GenContext, operation: &mut Operation) { + let schema = ctx.schema.subschema_for::(); + set_body( + ctx, + operation, + RequestBody { + description: Some("Multipart file upload".into()), + content: IndexMap::from_iter([( + "multipart/form-data".into(), + MediaType { + schema: Some(SchemaObject { + json_schema: schema.try_into().expect("invalid schema"), + external_docs: None, + example: None, + }), + ..Default::default() + }, + )]), + required: true, + extensions: IndexMap::default(), + }, + ); + } +} + +impl FromRequest for DocTypedMultipart +where + TypedMultipart: FromRequest, + S: Send + Sync, +{ + type Rejection = as FromRequest>::Rejection; + + async fn from_request(req: Request, state: &S) -> Result { + let TypedMultipart(inner) = TypedMultipart::from_request(req, state).await?; + Ok(Self(inner)) + } +}