vld-utoipa implements utoipa's PartialSchema, ToSchema and IntoParams for any type that has a vld json_schema() (vld::schema! or #[derive(Validate)]). minLength, minimum, formats and the other vld rules show up in the generated OpenAPI document without #[schema(...)] / #[param(...)] duplicates.
Install
[dependencies]
vld = { version = "0.5", features = ["openapi"] }
vld-utoipa = "0.5" # utoipa 5 (default)
utoipa = "5"
utoipa 6:
[dependencies]
vld-utoipa = { version = "0.5", default-features = false, features = ["utoipa-6"] }
utoipa = "6"
Features
| Feature | Default | Description |
|---|---|---|
utoipa-5 | yes | Build against utoipa 5.x |
utoipa-6 | no | Build against utoipa 6.x |
Enable exactly one of them; both or none is a compile error. The selected crate is re-exported as vld_utoipa::utoipa.
MSRV: 1.85 with utoipa-5, 1.88 with utoipa-6 (see MSRV).
Example
use vld::prelude::*;
use vld_utoipa::impl_to_schema;
// Nested type: registered as a component automatically.
vld::schema! {
#[derive(Debug)]
pub struct Address {
pub city: String => vld::string().min(1).max(100),
pub zip: String => vld::string().min(5).max(10),
}
}
impl_to_schema!(Address);
// JSON body
vld::schema! {
#[derive(Debug)]
pub struct CreateUser {
pub name: String => vld::string().min(2).max(100),
pub email: String => vld::string().email(),
pub age: Option<i64> => vld::number().int().gte(0).optional(),
pub address: Address => vld::nested!(Address),
}
}
impl_to_schema!(CreateUser);
// Query parameters: the location comes from utoipa's own attribute.
vld::schema! {
#[derive(Debug)]
#[into_params(parameter_in = Query)]
pub struct SearchQuery {
pub q: String => vld::string().min(1).max(200),
}
}
impl_to_schema!(SearchQuery);
#[utoipa::path(get, path = "/search", params(SearchQuery), responses((status = 200)))]
fn search_users() {}
#[utoipa::path(
post,
path = "/users",
request_body = CreateUser,
responses((status = 200), (status = 422, description = "Validation failed")),
)]
fn create_user() {}
// Address is not listed: it is collected through ToSchema::schemas().
#[derive(utoipa::OpenApi)]
#[openapi(paths(create_user, search_users), components(schemas(CreateUser)))]
struct ApiDoc;
The same macro works on #[derive(Validate)] structs; serde rename / rename_all are reflected in the property names:
#[derive(Debug, serde::Deserialize, vld::Validate)]
#[serde(rename_all = "camelCase")]
struct UpdateLocation {
#[vld(vld::string().min(1).max(255))]
street_address: String,
#[vld(vld::boolean())]
is_active: bool,
}
vld_utoipa::impl_to_schema!(UpdateLocation); // properties: streetAddress, isActive
API
| Item | Description |
|---|---|
impl_to_schema!(T) | PartialSchema + ToSchema + IntoParams for T |
impl_to_schema!(T, "Name") | Same, with a custom component name |
json_schema_to_schema(&Value) | JSON Schema value → utoipa::openapi::RefOr<Schema> |
json_schema_to_params(&Value, ParameterIn) | Object schema → Vec<Parameter> (one per property) |
parameter_in_from_str(&str) | "query" / "path" / "header" / "cookie" → ParameterIn |
utoipa | Re-export of the selected utoipa major |
prelude | The macros and functions above plus vld::prelude::* |
impl_to_schema!(T, query | path | header | cookie) sets the parameter location without the attribute; prefer #[into_params(parameter_in = …)] on the struct and impl_to_schema!(T).
Gotchas
IntoParamsreturns no parameters unless a location is known: utoipa's ownparams(...)provider, a legacy macro override, or#[into_params(parameter_in = …)]on the struct (checked in that order).- Parameter
requiredfollows the object'srequiredarray:.optional(),.nullish()and.with_default()fields are optional,.nullable()fields stay required. - Nested
vld::nested!types are registered transitively (grandchildren too). The nested type itself still needsimpl_to_schema!if you reference it in#[utoipa::path]directly. - Keywords utoipa has no field for (
contains,propertyNames,formatMinimum,not, …) and vld'sx-*extensions are kept as schema extensions;$refs withtitle/default/exampleare wrapped in a single-itemallOf. - utoipa's
#[utoipa::path]andOpenApiderive expect the crate to be namedutoipa; withutoipa-6addutoipa = "6"under that name. - Use one struct per HTTP role (body, query, path). OpenAPI describes them differently.
See also JSON Schema for what vld emits.