OpenAPI

JSON Schema and OpenAPI

Generate JSON Schema and OpenAPI 3.1 documents from vld schemas, including nested components and vld's x-* extensions.

With the openapi feature every built-in schema implements the JsonSchema trait. The output is a serde_json::Value in JSON Schema 2020-12, which OpenAPI 3.1 uses directly.

Cargo.toml
vld = { version = "0.5", features = ["openapi"] }
use vld::prelude::*; // brings in JsonSchema with the feature on

let email = vld::string().min(2).max(50).email();
let js = email.json_schema();
assert_eq!(js["type"], "string");
assert_eq!(js["minLength"], 2);
assert_eq!(js["format"], "email");

The JsonSchema trait

pub trait JsonSchema {
    fn json_schema(&self) -> serde_json::Value;
    fn is_required(&self) -> bool { true } // false for optional / nullable / nullish / with_default
}

ZObject also has an inherent to_json_schema() that returns the same value.

Structs

schema! and #[derive(Validate)] structs get two functions:

vld::schema! {
    #[derive(Debug)]
    pub struct CreateUser {
        pub name: String => vld::string().min(2).describe("Display name"),
        pub email: String => vld::string().email(),
        pub age: Option<u8> => vld::u8().optional(),
    }
}

let schema = CreateUser::json_schema();
assert_eq!(schema["required"], serde_json::json!(["name", "email"]));
assert_eq!(schema["properties"]["name"]["description"], "Display name");

let doc = CreateUser::to_openapi_document();
assert_eq!(doc["openapi"], "3.1.0");
assert!(doc["components"]["schemas"]["CreateUser"].is_object());

Optional, nullable, nullish and defaulted fields are left out of required.

Free functions

FunctionResult
vld::openapi::to_openapi_document(name, &schema)a minimal OpenAPI 3.1 document with the schema under #/components/schemas/{name}
vld::openapi::to_openapi_document_multi(&[(name, schema), …])the same for several schemas
vld::openapi::nullable_schema(inner){"anyOf": [inner, {"type": "null"}]}

The free functions do not add nested components; use the struct's to_openapi_document() for that.

Nested schemas

vld::nested(T::parse_value) embeds a nested type as {"type": "object"}. To reference it as a component, use vld::nested!(T):

vld::schema! {
    #[derive(Debug, Clone)]
    pub struct Address {
        pub city: String => vld::string().min(1),
    }
}

vld::schema! {
    #[derive(Debug)]
    pub struct Customer {
        pub address: Address => vld::nested!(Address),
    }
}

let doc = Customer::to_openapi_document();
assert_eq!(
    doc["components"]["schemas"]["Customer"]["properties"]["address"]["$ref"],
    "#/components/schemas/Address"
);
assert!(doc["components"]["schemas"]["Address"].is_object());

Nested components are collected transitively, so grandchildren are registered too. The same components are picked up by vld-utoipa, vld-schemars and vld-aide.

Mapping

vldJSON Schema
string().min(n).max(n)minLength / maxLength
string formatsformat (email, uri, uuid, ipv4, date-time, …)
regex(re) / starts_with / …pattern
number() / integers / f32()type: number / type: integer + format: int32… / format: float
min / gt / multiple_ofminimum / exclusiveMinimum / multipleOf
array().min_len() / unique() / contains()minItems / uniqueItems / contains + minContains
tuplesprefixItems
record() / record_with_keys()additionalProperties, propertyNames, minProperties / maxProperties
literal(v)const
enumeration() / enum_of()enum
optional() / nullable() / nullish()anyOf: [inner, {"type": "null"}]
with_default(v)default
describe(text)description
unionsanyOf (first match wins, so oneOf would be wrong)
discriminated_unionanyOf of the variant schemas

vld extensions

Some constraints have no JSON Schema keyword. vld emits them as x-* keys, which OpenAPI tools ignore and diff understands:

KeyEmitted by
x-past, x-futuredate() / datetime() .past() / .future()
x-minDecimal, x-maxDecimal, x-exclusiveMinDecimal, x-exclusiveMaxDecimal, x-multipleOfDecimaldecimal() bounds (as strings)
x-minSeconds, x-maxSecondsduration() bounds
x-minFileSize, x-maxFileSizefile() size bounds
x-minUniqueItems, x-maxUniqueItemsset() size bounds
x-maxDepthjson_value().max_depth(n)

Dates also use the standard-track formatMinimum / formatMaximum for min / max.