Skip to main content

vld_schemars/
lib.rs

1//! # vld-schemars — Bidirectional bridge between `vld` and `schemars`
2//!
3//! Many Rust libraries (aide, paperclip, okapi, utoipa-rapidoc, etc.) already use
4//! [`schemars`](https://docs.rs/schemars) for JSON Schema generation. This crate
5//! lets you share schema definitions between `vld` and the broader `schemars`
6//! ecosystem — in **both** directions.
7//!
8//! ## vld → schemars
9//!
10//! ```rust
11//! use vld::prelude::*;
12//! use vld_schemars::impl_json_schema;
13//!
14//! vld::schema! {
15//!     #[derive(Debug)]
16//!     pub struct User {
17//!         pub name: String => vld::string().min(2).max(50),
18//!         pub email: String => vld::string().email(),
19//!     }
20//! }
21//!
22//! impl_json_schema!(User);
23//! // User now implements schemars::JsonSchema
24//! ```
25//!
26//! ## schemars → vld (macro on type)
27//!
28//! ```rust
29//! use vld_schemars::{impl_vld_parse, SchemarsValidate};
30//!
31//! #[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
32//! struct User { name: String, age: u32 }
33//!
34//! impl_vld_parse!(User);
35//!
36//! let user = User { name: "Alice".into(), age: 30 };
37//! user.vld_validate().unwrap(); // validate existing instance
38//!
39//! let json = serde_json::json!({"name": "Alice", "age": 30});
40//! let user = User::vld_parse(&json).unwrap(); // validate + deserialize
41//! ```
42
43use serde_json::Value;
44use std::fmt;
45
46pub use schemars;
47pub use vld;
48
49// ========================= Error type ========================================
50
51/// Error returned by schemars → vld validation.
52#[derive(Debug, Clone)]
53pub enum VldSchemarsError {
54    /// JSON Schema validation failed.
55    Validation(vld::error::VldError),
56    /// Deserialization failed.
57    Deserialization(String),
58}
59
60impl fmt::Display for VldSchemarsError {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        match self {
63            VldSchemarsError::Validation(e) => write!(f, "Schema validation error: {}", e),
64            VldSchemarsError::Deserialization(e) => write!(f, "Deserialization error: {}", e),
65        }
66    }
67}
68
69impl std::error::Error for VldSchemarsError {
70    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
71        match self {
72            VldSchemarsError::Validation(e) => Some(e),
73            VldSchemarsError::Deserialization(_) => None,
74        }
75    }
76}
77
78impl From<vld::error::VldError> for VldSchemarsError {
79    fn from(e: vld::error::VldError) -> Self {
80        VldSchemarsError::Validation(e)
81    }
82}
83
84// ========================= vld → schemars ====================================
85
86/// Convert a `serde_json::Value` (JSON Schema produced by `vld`) into a
87/// `schemars::Schema`.
88///
89/// ```rust
90/// let schema = vld_schemars::vld_to_schemars(&serde_json::json!({"type": "string"}));
91/// assert_eq!(schema.get("type").unwrap(), "string");
92/// ```
93pub fn vld_to_schemars(value: &Value) -> schemars::Schema {
94    value
95        .clone()
96        .try_into()
97        .unwrap_or_else(|_| schemars::Schema::default())
98}
99
100/// Generate a `schemars::Schema` from a vld type that has a `json_schema()` method.
101///
102/// Works with types defined via `vld::schema!` (with `openapi` feature) or `#[derive(Validate)]`.
103///
104/// ```rust
105/// use vld::prelude::*;
106/// use vld::json_schema::JsonSchema;
107///
108/// let schema = vld_schemars::vld_schema_to_schemars(&vld::string().email().json_schema());
109/// assert_eq!(schema.get("type").unwrap(), "string");
110/// assert_eq!(schema.get("format").unwrap(), "email");
111/// ```
112pub fn vld_schema_to_schemars(vld_json: &Value) -> schemars::Schema {
113    vld_to_schemars(vld_json)
114}
115
116// ========================= schemars → vld ====================================
117
118/// Convert a `schemars::Schema` to a `serde_json::Value`.
119///
120/// The resulting JSON value is a standard JSON Schema that can be used for
121/// documentation, comparison, or feeding into other tools.
122///
123/// ```rust
124/// use schemars::JsonSchema;
125///
126/// let schemars_schema = schemars::SchemaGenerator::default().into_root_schema_for::<String>();
127/// let json = vld_schemars::schemars_to_json(&schemars_schema);
128/// assert!(json.is_object());
129/// ```
130pub fn schemars_to_json(schema: &schemars::Schema) -> Value {
131    schema.as_value().clone()
132}
133
134/// Generate a vld-compatible JSON Schema value from a type implementing
135/// `schemars::JsonSchema`.
136///
137/// ```rust
138/// let schema = vld_schemars::generate_from_schemars::<String>();
139/// assert_eq!(schema["type"], "string");
140/// ```
141pub fn generate_from_schemars<T: schemars::JsonSchema>() -> Value {
142    let schema = schemars::SchemaGenerator::default().into_root_schema_for::<T>();
143    schemars_to_json(&schema)
144}
145
146/// Generate a root `schemars::Schema` for a type implementing `schemars::JsonSchema`.
147///
148/// Convenience wrapper around `SchemaGenerator::into_root_schema_for`.
149///
150/// ```rust
151/// let schema = vld_schemars::generate_schemars::<i32>();
152/// assert!(schema.get("type").is_some());
153/// ```
154pub fn generate_schemars<T: schemars::JsonSchema>() -> schemars::Schema {
155    schemars::SchemaGenerator::default().into_root_schema_for::<T>()
156}
157
158// ========================= schemars → vld validation =========================
159
160mod validator;
161
162/// Validate a `serde_json::Value` against a JSON Schema.
163///
164/// Performs structural validation: type checks, required fields,
165/// string constraints (minLength, maxLength, pattern, format),
166/// number constraints (minimum, maximum, exclusiveMinimum, exclusiveMaximum),
167/// array constraints (minItems, maxItems), enum values, and recursive
168/// validation of `properties` and `items`.
169///
170/// ```rust
171/// let schema = serde_json::json!({
172///     "type": "object",
173///     "required": ["name"],
174///     "properties": {
175///         "name": { "type": "string", "minLength": 1 },
176///         "age":  { "type": "integer", "minimum": 0 }
177///     }
178/// });
179/// let valid = serde_json::json!({"name": "Alice", "age": 30});
180/// assert!(vld_schemars::validate_with_schema(&schema, &valid).is_ok());
181///
182/// let invalid = serde_json::json!({"age": -5});
183/// assert!(vld_schemars::validate_with_schema(&schema, &invalid).is_err());
184/// ```
185pub fn validate_with_schema(schema: &Value, value: &Value) -> Result<(), vld::error::VldError> {
186    validator::validate_value_against_schema(schema, value, &[])
187}
188
189/// Validate a `serde_json::Value` against a `schemars::Schema`.
190///
191/// ```rust
192/// let schema = vld_schemars::vld_to_schemars(&serde_json::json!({
193///     "type": "string", "minLength": 2
194/// }));
195/// assert!(vld_schemars::validate_with_schemars(&schema, &serde_json::json!("hello")).is_ok());
196/// assert!(vld_schemars::validate_with_schemars(&schema, &serde_json::json!("x")).is_err());
197/// ```
198pub fn validate_with_schemars(
199    schema: &schemars::Schema,
200    value: &Value,
201) -> Result<(), vld::error::VldError> {
202    validate_with_schema(schema.as_value(), value)
203}
204
205// ========================= SchemarsValidate trait ============================
206
207/// Trait for types that can be validated using their `schemars::JsonSchema`.
208///
209/// Automatically implemented by [`impl_vld_parse!`].
210///
211/// ```rust
212/// use vld_schemars::{impl_vld_parse, SchemarsValidate};
213///
214/// #[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
215/// struct User { name: String, age: u32 }
216///
217/// impl_vld_parse!(User);
218///
219/// let user = User { name: "Alice".into(), age: 30 };
220/// assert!(user.vld_validate().is_ok());
221/// ```
222pub trait SchemarsValidate: schemars::JsonSchema + serde::Serialize + Sized {
223    /// Validate this value against its schemars-generated JSON Schema.
224    fn vld_validate(&self) -> Result<(), vld::error::VldError> {
225        let schema = generate_schemars::<Self>();
226        let json = serde_json::to_value(self).map_err(|e| {
227            vld::error::VldError::single(
228                vld::error::IssueCode::ParseError,
229                format!("Serialization error: {}", e),
230            )
231        })?;
232        validate_with_schemars(&schema, &json)
233    }
234
235    /// Validate a JSON value against this type's schemars schema.
236    fn vld_validate_json(value: &Value) -> Result<(), vld::error::VldError> {
237        let schema = generate_schemars::<Self>();
238        validate_with_schemars(&schema, value)
239    }
240
241    /// Validate and deserialize a JSON value into this type.
242    fn vld_parse(value: &Value) -> Result<Self, VldSchemarsError>
243    where
244        Self: serde::de::DeserializeOwned,
245    {
246        Self::vld_validate_json(value).map_err(VldSchemarsError::Validation)?;
247        serde_json::from_value(value.clone())
248            .map_err(|e| VldSchemarsError::Deserialization(e.to_string()))
249    }
250}
251
252/// Implement `vld::schema::VldParse` and [`SchemarsValidate`] for a type
253/// that derives `schemars::JsonSchema`, `serde::Serialize`, and `serde::Deserialize`.
254///
255/// This is the **reverse** of [`impl_json_schema!`]: given a schemars type,
256/// make it usable with vld's validation framework (extractors, etc.).
257///
258/// # What it gives you
259///
260/// - `vld::schema::VldParse` — the type can be used with vld extractors
261///   (`vld-axum`, `vld-actix`, etc.)
262/// - `SchemarsValidate` — validate existing instances via `.vld_validate()`,
263///   validate JSON via `Type::vld_validate_json(&json)`,
264///   parse via `Type::vld_parse(&json)`
265///
266/// # Example
267///
268/// ```rust
269/// use vld_schemars::{impl_vld_parse, SchemarsValidate};
270///
271/// #[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
272/// struct Item {
273///     name: String,
274///     qty: u32,
275/// }
276///
277/// impl_vld_parse!(Item);
278///
279/// // Validate existing instance
280/// let item = Item { name: "Widget".into(), qty: 5 };
281/// assert!(item.vld_validate().is_ok());
282///
283/// // Parse from JSON (validate + deserialize)
284/// let json = serde_json::json!({"name": "Widget", "qty": 5});
285/// let item = Item::vld_parse(&json).unwrap();
286/// assert_eq!(item.name, "Widget");
287///
288/// // Use with vld::VldParse
289/// use vld::schema::VldParse;
290/// let item = Item::vld_parse_value(&json).unwrap();
291/// assert_eq!(item.qty, 5);
292/// ```
293#[macro_export]
294macro_rules! impl_vld_parse {
295    ($ty:ty) => {
296        impl $crate::vld::schema::VldParse for $ty {
297            fn vld_parse_value(
298                value: &::serde_json::Value,
299            ) -> ::std::result::Result<Self, $crate::vld::error::VldError> {
300                let schema = $crate::generate_schemars::<$ty>();
301                $crate::validate_with_schemars(&schema, value)?;
302                ::serde_json::from_value(value.clone()).map_err(|e| {
303                    $crate::vld::error::VldError::single(
304                        $crate::vld::error::IssueCode::ParseError,
305                        format!("Deserialization error: {}", e),
306                    )
307                })
308            }
309        }
310
311        impl $crate::SchemarsValidate for $ty {}
312    };
313}
314
315// ========================= Introspection =====================================
316
317/// Information about a single property in a JSON Schema object.
318#[derive(Debug, Clone, PartialEq, Eq)]
319pub struct PropertyInfo {
320    /// Property name.
321    pub name: String,
322    /// JSON Schema "type" value (e.g. "string", "integer", "number", "boolean", "object", "array").
323    pub schema_type: Option<String>,
324    /// Whether this property is listed in "required".
325    pub required: bool,
326    /// The raw JSON schema for this property.
327    pub schema: Value,
328}
329
330/// Extract property information from a JSON Schema object.
331///
332/// Works with both `schemars::Schema` (via `as_value()`) and raw `serde_json::Value`.
333///
334/// ```rust
335/// use vld::prelude::*;
336///
337/// vld::schema! {
338///     #[derive(Debug)]
339///     pub struct UserSchema {
340///         pub name: String => vld::string().min(1),
341///         pub age: i64 => vld::number().int().min(0),
342///     }
343/// }
344///
345/// let json = UserSchema::json_schema();
346/// let props = vld_schemars::list_properties(&json);
347/// assert_eq!(props.len(), 2);
348/// ```
349pub fn list_properties(schema: &Value) -> Vec<PropertyInfo> {
350    let mut result = Vec::new();
351
352    let properties = match schema.get("properties").and_then(|p| p.as_object()) {
353        Some(p) => p,
354        None => return result,
355    };
356
357    let required_set: std::collections::HashSet<&str> = schema
358        .get("required")
359        .and_then(|r| r.as_array())
360        .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
361        .unwrap_or_default();
362
363    for (name, prop_schema) in properties {
364        let schema_type = prop_schema
365            .get("type")
366            .and_then(|t| t.as_str())
367            .map(String::from);
368
369        result.push(PropertyInfo {
370            name: name.clone(),
371            schema_type,
372            required: required_set.contains(name.as_str()),
373            schema: prop_schema.clone(),
374        });
375    }
376
377    result
378}
379
380/// Extract property info from a `schemars::Schema`.
381pub fn list_properties_schemars(schema: &schemars::Schema) -> Vec<PropertyInfo> {
382    list_properties(schema.as_value())
383}
384
385/// Get the "type" field from a JSON Schema.
386///
387/// ```rust
388/// let schema = serde_json::json!({"type": "string"});
389/// assert_eq!(vld_schemars::schema_type(&schema), Some("string".to_string()));
390/// ```
391pub fn schema_type(schema: &Value) -> Option<String> {
392    schema
393        .get("type")
394        .and_then(|t| t.as_str())
395        .map(String::from)
396}
397
398/// Check if a field is required in a JSON Schema.
399///
400/// ```rust
401/// let schema = serde_json::json!({
402///     "type": "object",
403///     "required": ["name"],
404///     "properties": { "name": { "type": "string" } }
405/// });
406/// assert!(vld_schemars::is_required(&schema, "name"));
407/// assert!(!vld_schemars::is_required(&schema, "age"));
408/// ```
409pub fn is_required(schema: &Value, field: &str) -> bool {
410    schema
411        .get("required")
412        .and_then(|r| r.as_array())
413        .map(|arr| arr.iter().any(|v| v.as_str() == Some(field)))
414        .unwrap_or(false)
415}
416
417/// Get the property schema for a specific field.
418///
419/// ```rust
420/// let schema = serde_json::json!({
421///     "type": "object",
422///     "properties": { "name": { "type": "string", "minLength": 1 } }
423/// });
424/// let name_schema = vld_schemars::get_property(&schema, "name").unwrap();
425/// assert_eq!(name_schema["type"], "string");
426/// ```
427pub fn get_property<'a>(schema: &'a Value, field: &str) -> Option<&'a Value> {
428    schema
429        .get("properties")
430        .and_then(|p| p.as_object())
431        .and_then(|props| props.get(field))
432}
433
434// ========================= Comparison & Merge ================================
435
436/// Check if two JSON Schema values are structurally equal.
437///
438/// ```rust
439/// use vld::prelude::*;
440/// use vld::json_schema::JsonSchema;
441///
442/// let a = vld::string().min(1).json_schema();
443/// let b = serde_json::json!({"type": "string", "minLength": 1});
444/// assert!(vld_schemars::schemas_equal(&a, &b));
445/// ```
446pub fn schemas_equal(a: &Value, b: &Value) -> bool {
447    a == b
448}
449
450/// Merge two `schemars::Schema` into one using `allOf`.
451///
452/// ```rust
453/// let a = vld_schemars::vld_to_schemars(&serde_json::json!({"type": "object", "properties": {"name": {"type": "string"}}}));
454/// let b = vld_schemars::vld_to_schemars(&serde_json::json!({"type": "object", "properties": {"age": {"type": "integer"}}}));
455/// let merged = vld_schemars::merge_schemas(&a, &b);
456/// assert!(merged.get("allOf").is_some());
457/// ```
458pub fn merge_schemas(a: &schemars::Schema, b: &schemars::Schema) -> schemars::Schema {
459    let merged = serde_json::json!({
460        "allOf": [a.as_value(), b.as_value()]
461    });
462    vld_to_schemars(&merged)
463}
464
465/// Overlay additional constraints from one schema onto another.
466///
467/// Copies `properties`, `required`, and validation keywords (`minLength`,
468/// `maxLength`, `minimum`, `maximum`, `pattern`, `format`) from `overlay`
469/// into a clone of `base`. Existing `base` values are preserved.
470///
471/// ```rust
472/// let base = serde_json::json!({"type": "object", "properties": {"name": {"type": "string"}}});
473/// let overlay = serde_json::json!({"properties": {"name": {"minLength": 2}}, "required": ["name"]});
474/// let result = vld_schemars::overlay_constraints(&base, &overlay);
475/// assert!(vld_schemars::is_required(&result, "name"));
476/// ```
477pub fn overlay_constraints(base: &Value, overlay: &Value) -> Value {
478    let mut result = base.clone();
479
480    if let (Some(result_obj), Some(overlay_obj)) = (result.as_object_mut(), overlay.as_object()) {
481        for (key, value) in overlay_obj {
482            match key.as_str() {
483                "properties" => {
484                    if let (Some(base_props), Some(overlay_props)) = (
485                        result_obj
486                            .get_mut("properties")
487                            .and_then(|p| p.as_object_mut()),
488                        value.as_object(),
489                    ) {
490                        for (prop_name, prop_schema) in overlay_props {
491                            if let Some(existing) = base_props.get_mut(prop_name) {
492                                if let (Some(existing_obj), Some(overlay_prop_obj)) =
493                                    (existing.as_object_mut(), prop_schema.as_object())
494                                {
495                                    for (k, v) in overlay_prop_obj {
496                                        existing_obj.entry(k.clone()).or_insert_with(|| v.clone());
497                                    }
498                                }
499                            } else {
500                                base_props.insert(prop_name.clone(), prop_schema.clone());
501                            }
502                        }
503                    }
504                }
505                "required" => {
506                    if let Some(overlay_required) = value.as_array() {
507                        let base_required = result_obj
508                            .entry("required")
509                            .or_insert_with(|| Value::Array(vec![]));
510                        if let Some(arr) = base_required.as_array_mut() {
511                            for item in overlay_required {
512                                if !arr.contains(item) {
513                                    arr.push(item.clone());
514                                }
515                            }
516                        }
517                    }
518                }
519                _ => {
520                    result_obj
521                        .entry(key.clone())
522                        .or_insert_with(|| value.clone());
523                }
524            }
525        }
526    }
527
528    result
529}
530
531// ========================= impl_json_schema! =================================
532
533/// Implement `schemars::JsonSchema` for a type that has a `json_schema()`
534/// associated function (generated by `vld::schema!` or `#[derive(Validate)]`).
535///
536/// # Usage
537///
538/// ```rust
539/// use vld::prelude::*;
540/// use vld_schemars::impl_json_schema;
541///
542/// vld::schema! {
543///     #[derive(Debug)]
544///     pub struct CreateUser {
545///         pub name: String => vld::string().min(2).max(100),
546///         pub email: String => vld::string().email(),
547///     }
548/// }
549///
550/// impl_json_schema!(CreateUser);
551///
552/// // Now CreateUser implements schemars::JsonSchema
553/// ```
554///
555/// With a custom schema name:
556///
557/// ```rust
558/// # use vld::prelude::*;
559/// # use vld_schemars::impl_json_schema;
560/// # vld::schema! {
561/// #     #[derive(Debug)]
562/// #     pub struct Req { pub x: String => vld::string() }
563/// # }
564/// impl_json_schema!(Req, "CreateUserRequest");
565/// ```
566#[macro_export]
567macro_rules! impl_json_schema {
568    ($ty:ty) => {
569        impl $crate::schemars::JsonSchema for $ty {
570            fn schema_name() -> ::std::borrow::Cow<'static, str> {
571                ::std::borrow::Cow::Borrowed(stringify!($ty))
572            }
573
574            fn schema_id() -> ::std::borrow::Cow<'static, str> {
575                ::std::borrow::Cow::Owned(concat!(module_path!(), "::", stringify!($ty)).to_owned())
576            }
577
578            fn json_schema(
579                gen: &mut $crate::schemars::SchemaGenerator,
580            ) -> $crate::schemars::Schema {
581                #[allow(unused_imports)]
582                use $crate::__VldNestedSchemasFallback as _;
583                for (name, schema_fn) in <$ty>::__vld_nested_schemas() {
584                    gen.definitions_mut()
585                        .entry(name.to_string())
586                        .or_insert_with(|| schema_fn());
587                }
588                $crate::vld_to_schemars(&<$ty>::json_schema())
589            }
590        }
591    };
592    ($ty:ty, $name:expr) => {
593        impl $crate::schemars::JsonSchema for $ty {
594            fn schema_name() -> ::std::borrow::Cow<'static, str> {
595                ::std::borrow::Cow::Borrowed($name)
596            }
597
598            fn schema_id() -> ::std::borrow::Cow<'static, str> {
599                ::std::borrow::Cow::Owned(format!("{}::{}", module_path!(), $name))
600            }
601
602            fn json_schema(
603                gen: &mut $crate::schemars::SchemaGenerator,
604            ) -> $crate::schemars::Schema {
605                #[allow(unused_imports)]
606                use $crate::__VldNestedSchemasFallback as _;
607                for (name, schema_fn) in <$ty>::__vld_nested_schemas() {
608                    gen.definitions_mut()
609                        .entry(name.to_string())
610                        .or_insert_with(|| schema_fn());
611                }
612                $crate::vld_to_schemars(&<$ty>::json_schema())
613            }
614        }
615    };
616}
617
618#[doc(hidden)]
619pub trait __VldNestedSchemasFallback {
620    #[allow(clippy::type_complexity)]
621    fn __vld_nested_schemas() -> Vec<(&'static str, fn() -> serde_json::Value)> {
622        Vec::new()
623    }
624}
625
626impl<T: ?Sized> __VldNestedSchemasFallback for T {}
627
628// ========================= Prelude ===========================================
629
630pub mod prelude {
631    pub use crate::impl_json_schema;
632    pub use crate::impl_vld_parse;
633    pub use crate::{
634        generate_from_schemars, generate_schemars, get_property, is_required, list_properties,
635        list_properties_schemars, merge_schemas, overlay_constraints, schema_type,
636        schemars_to_json, schemas_equal, validate_with_schema, validate_with_schemars,
637        vld_schema_to_schemars, vld_to_schemars, PropertyInfo, SchemarsValidate, VldSchemarsError,
638    };
639    pub use vld::prelude::*;
640}