Skip to main content

vld/schema/
mod.rs

1use serde_json::Value;
2
3use crate::combinators::{
4    ZCatch, ZDescribe, ZIntersection, ZPipe, ZRefine, ZSuperRefine, ZTransform, ZUnion2,
5};
6use crate::error::VldError;
7use crate::modifiers::{ZDefault, ZNullable, ZNullish, ZOptional};
8
9mod coerced;
10mod dyn_schema;
11/// Input sources accepted by `parse` (JSON text, files, values).
12pub mod input;
13mod nested;
14
15pub(crate) use dyn_schema::to_json_value;
16
17pub use coerced::parse_coerced;
18pub use dyn_schema::DynSchema;
19pub use input::VldInput;
20pub use nested::{NestedSchema, NestedSchemaCollectFn, NestedSchemaEntry};
21
22/// A schema with everything the enabled features need: [`VldSchema`], plus
23/// [`JsonSchema`](crate::openapi::JsonSchema) and
24/// [`CollectNestedSchemas`](crate::openapi::CollectNestedSchemas) with the
25/// `openapi` feature.
26///
27/// Use it to return a schema from a function without naming its type: an
28/// `impl VldSchema` hides the JSON Schema impls, so the result could not be
29/// used in an object or `schema!` field with `openapi` on.
30///
31/// ```
32/// use vld::prelude::*;
33///
34/// fn username() -> impl vld::Schema<Output = String> {
35///     vld::string().min(3).max(20).alphanumeric()
36/// }
37///
38/// let user = vld::object().field("name", username());
39/// assert!(user.parse(r#"{"name": "ada"}"#).is_ok());
40/// ```
41#[cfg(feature = "openapi")]
42pub trait Schema:
43    VldSchema + crate::openapi::JsonSchema + crate::openapi::CollectNestedSchemas
44{
45}
46
47#[cfg(feature = "openapi")]
48impl<T> Schema for T where
49    T: VldSchema + crate::openapi::JsonSchema + crate::openapi::CollectNestedSchemas
50{
51}
52
53/// A schema with everything the enabled features need (here: [`VldSchema`];
54/// with `openapi` also the JSON Schema traits).
55#[cfg(not(feature = "openapi"))]
56pub trait Schema: VldSchema {}
57
58#[cfg(not(feature = "openapi"))]
59impl<T: VldSchema> Schema for T {}
60
61/// Core validation schema trait.
62///
63/// Every validator in `vld` implements this trait. The associated type `Output`
64/// defines what Rust type will be produced after successful parsing.
65///
66/// # Example
67/// ```
68/// use vld::prelude::*;
69///
70/// let schema = vld::string().min(3);
71/// let result = schema.parse(r#""hello""#);
72/// assert!(result.is_ok());
73/// ```
74pub trait VldSchema: Sized {
75    /// The Rust type produced by this schema after successful parsing.
76    type Output;
77
78    /// Parse and validate a `serde_json::Value`.
79    fn parse_value(&self, value: &Value) -> Result<Self::Output, VldError>;
80
81    /// The JSON form of an output value, when it differs from its `serde`
82    /// form (e.g. `time` datetimes serialize as non-RFC 3339 text). Used for
83    /// the JSON Schema `default` of `.with_default(...)`; `None` means use
84    /// `serde_json::to_value`.
85    #[doc(hidden)]
86    fn output_json(&self, _output: &Self::Output) -> Option<Value> {
87        None
88    }
89
90    /// Parse an object field whose key is **absent**.
91    ///
92    /// Default: the same as an explicit `null` (`parse_value(&Value::Null)`),
93    /// so an absent key and `null` are treated alike. `nullable()` overrides
94    /// it to require the key (`missing_field`); wrappers (`describe`,
95    /// `refine`, `transform`, `rule`, …) forward it to their inner schema.
96    fn parse_missing(&self) -> Result<Self::Output, VldError> {
97        self.parse_value(&Value::Null)
98    }
99
100    /// Parse from any supported input (JSON string, file path, `serde_json::Value`, etc.)
101    fn parse<I: VldInput + ?Sized>(&self, input: &I) -> Result<Self::Output, VldError> {
102        let json = input.to_json_value()?;
103        self.parse_value(&json)
104    }
105
106    /// Validate an existing Rust value against this schema.
107    ///
108    /// The value is serialized to JSON via `serde`, then validated.
109    /// Returns the parsed output on success.
110    ///
111    /// # Example
112    /// ```
113    /// use vld::prelude::*;
114    ///
115    /// let schema = vld::array(vld::number().int().positive()).min_len(1);
116    /// let data = vec![1, 2, 3];
117    /// assert!(schema.validate(&data).is_ok());
118    /// ```
119    fn validate<T: serde::Serialize>(&self, value: &T) -> Result<Self::Output, VldError> {
120        self.parse_value(&to_json_value(value)?)
121    }
122
123    /// Check if an existing Rust value passes this schema's validation.
124    ///
125    /// # Example
126    /// ```
127    /// use vld::prelude::*;
128    ///
129    /// let schema = vld::string().email();
130    /// assert!(schema.is_valid(&"user@example.com"));
131    /// ```
132    fn is_valid<T: serde::Serialize>(&self, value: &T) -> bool {
133        self.validate(value).is_ok()
134    }
135
136    /// Make this field optional. Missing or null values become `None`.
137    fn optional(self) -> ZOptional<Self> {
138        ZOptional::new(self)
139    }
140
141    /// Allow null values. Null becomes `None`.
142    fn nullable(self) -> ZNullable<Self> {
143        ZNullable::new(self)
144    }
145
146    /// Provide a default value when the field is missing or null.
147    fn with_default(self, value: Self::Output) -> ZDefault<Self>
148    where
149        Self::Output: Clone,
150    {
151        ZDefault::new(self, value)
152    }
153
154    /// Add a custom refinement check without changing the output type.
155    fn refine<F>(self, check: F, message: &str) -> ZRefine<Self, F>
156    where
157        F: Fn(&Self::Output) -> bool,
158    {
159        ZRefine::new(self, check, message)
160    }
161
162    /// Check the output with a reusable [`Rule`](crate::rules::Rule) (output type unchanged).
163    ///
164    /// ```
165    /// use vld::prelude::*;
166    /// let even = vld::number().int().rule(vld::rules::from_fn("even", |n: &i64| n % 2 == 0));
167    /// assert_eq!(even.parse("3").unwrap_err().issues[0].code.key(), "even");
168    /// ```
169    fn rule<R>(self, rule: R) -> crate::rules::ZRule<Self, R>
170    where
171        Self: Sized,
172        R: crate::rules::Rule<Self::Output>,
173    {
174        crate::rules::ZRule::new(self, rule, None)
175    }
176
177    /// [`rule`](Self::rule) with a custom error message.
178    fn rule_msg<R>(self, rule: R, msg: impl Into<String>) -> crate::rules::ZRule<Self, R>
179    where
180        Self: Sized,
181        R: crate::rules::Rule<Self::Output>,
182    {
183        crate::rules::ZRule::new(self, rule, Some(msg.into()))
184    }
185
186    /// Transform the output with a fallible function; an `Err` becomes a
187    /// validation issue (`Custom { code: "transform" }`, message = the error).
188    ///
189    /// ```
190    /// use vld::prelude::*;
191    /// let big = vld::string().try_transform(|s| s.parse::<u128>());
192    /// assert_eq!(big.parse(r#""340282366920938463463374607431768211455""#).unwrap(), u128::MAX);
193    /// assert_eq!(big.parse(r#""x""#).unwrap_err().issues[0].code.key(), "transform");
194    /// ```
195    fn try_transform<F, U, E>(self, f: F) -> crate::combinators::ZTryTransform<Self, F, U>
196    where
197        F: Fn(Self::Output) -> Result<U, E>,
198        E: std::fmt::Display,
199    {
200        crate::combinators::ZTryTransform::new(self, f)
201    }
202
203    /// Transform the output value after successful parsing.
204    fn transform<F, U>(self, f: F) -> ZTransform<Self, F, U>
205    where
206        F: Fn(Self::Output) -> U,
207    {
208        ZTransform::new(self, f)
209    }
210
211    /// Make this field nullish (both optional and nullable).
212    fn nullish(self) -> ZNullish<Self> {
213        ZNullish::new(self)
214    }
215
216    /// Return a fallback value on ANY validation error.
217    fn catch(self, fallback: Self::Output) -> ZCatch<Self>
218    where
219        Self::Output: Clone,
220    {
221        ZCatch::new(self, fallback)
222    }
223
224    /// Chain this schema's output into another schema.
225    ///
226    /// The output of `self` is serialized to JSON, then parsed by `next`.
227    fn pipe<S: VldSchema>(self, next: S) -> ZPipe<Self, S>
228    where
229        Self::Output: serde::Serialize,
230    {
231        ZPipe::new(self, next)
232    }
233
234    /// Attach a human-readable description/label to this schema.
235    ///
236    /// The description is stored as metadata and does not affect validation.
237    fn describe(self, description: &str) -> ZDescribe<Self> {
238        ZDescribe::new(self, description)
239    }
240
241    /// Add a custom refinement that can produce multiple errors.
242    ///
243    /// Unlike `refine()` which returns a single bool, `super_refine` receives
244    /// a mutable `VldError` collector and can push multiple issues.
245    fn super_refine<F>(self, check: F) -> ZSuperRefine<Self, F>
246    where
247        F: Fn(&Self::Output, &mut VldError),
248    {
249        ZSuperRefine::new(self, check)
250    }
251
252    /// Create a union: this schema **or** another. Returns `Either<Self::Output, B::Output>`.
253    fn or<B: VldSchema>(self, other: B) -> ZUnion2<Self, B> {
254        ZUnion2::new(self, other)
255    }
256
257    /// Create an intersection: input must satisfy **both** schemas.
258    fn and<B: VldSchema>(self, other: B) -> ZIntersection<Self, B> {
259        ZIntersection::new(self, other)
260    }
261
262    /// Override the error message for this schema.
263    ///
264    /// On validation failure **all** issues produced by the inner schema
265    /// will have their message replaced with the provided string.
266    ///
267    /// Similar to Zod's `.message("...")`.
268    ///
269    /// # Example
270    /// ```
271    /// use vld::prelude::*;
272    ///
273    /// let schema = vld::string().min(3).message("Too short");
274    /// let err = schema.parse(r#""ab""#).unwrap_err();
275    /// assert_eq!(err.issues[0].message, "Too short");
276    /// ```
277    fn message(self, msg: impl Into<String>) -> crate::combinators::ZMessage<Self> {
278        crate::combinators::ZMessage::new(self, msg)
279    }
280}
281
282/// Trait for types that can be parsed from a `serde_json::Value`.
283///
284/// Auto-implemented by the [`schema!`](crate::schema!) macro and
285/// [`#[derive(Validate)]`](crate::Validate) derive macro.
286///
287/// Used by framework integration crates (e.g., `vld-axum`, `vld-actix`)
288/// to provide type-safe request body extraction.
289pub trait VldParse: Sized {
290    /// Parse and validate a `serde_json::Value` into this type.
291    fn vld_parse_value(value: &serde_json::Value) -> Result<Self, crate::error::VldError>;
292}