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}