OpenAPI and JSON Schema
Use vld schemas as utoipa ToSchema / IntoParams types, so validation rules and OpenAPI docs come from one definition.

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

Cargo.toml
[dependencies]
vld = { version = "0.5", features = ["openapi"] }
vld-utoipa = "0.5"   # utoipa 5 (default)
utoipa = "5"

utoipa 6:

Cargo.toml
[dependencies]
vld-utoipa = { version = "0.5", default-features = false, features = ["utoipa-6"] }
utoipa = "6"

Features

FeatureDefaultDescription
utoipa-5yesBuild against utoipa 5.x
utoipa-6noBuild 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

ItemDescription
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
utoipaRe-export of the selected utoipa major
preludeThe 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

  • IntoParams returns no parameters unless a location is known: utoipa's own params(...) provider, a legacy macro override, or #[into_params(parameter_in = …)] on the struct (checked in that order).
  • Parameter required follows the object's required array: .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 needs impl_to_schema! if you reference it in #[utoipa::path] directly.
  • Keywords utoipa has no field for (contains, propertyNames, formatMinimum, not, …) and vld's x-* extensions are kept as schema extensions; $refs with title / default / example are wrapped in a single-item allOf.
  • utoipa's #[utoipa::path] and OpenApi derive expect the crate to be named utoipa; with utoipa-6 add utoipa = "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.