vld 0.5.1

Type-safe runtime validation library for Rust, inspired by Zod
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
/// Helper macro to resolve a field's JSON key.
///
/// If a rename literal is provided, use it; otherwise fall back to the field name.
#[doc(hidden)]
#[macro_export]
macro_rules! __vld_resolve_key {
    ($default:expr) => {
        $default
    };
    ($default:expr, $override:expr) => {
        $override
    };
}

/// Filter `#[into_params(...)]` off struct attributes, then emit attrs + items together.
#[doc(hidden)]
#[macro_export]
macro_rules! __vld_struct_attrs {
    ( attrs: $(#[ $($attr:tt)* ])*
      body: { $($body:tt)* }
    ) => {
        $crate::__vld_struct_attrs!(@filter [] $(#[ $($attr)* ])*
            body: { $($body)* }
        );
    };
    (
        @filter [$($acc:tt)*]
        #[ into_params ( $($p:tt)* ) ]
        $(#[ $($rest:tt)* ])*
        body: { $($body:tt)* }
    ) => {
        $crate::__vld_struct_attrs!(@filter [$($acc)*]
            $(#[ $($rest)* ])*
            body: { $($body)* }
        );
    };
    (
        @filter [$($acc:tt)*]
        #[ $($attr:tt)* ]
        $(#[ $($rest:tt)* ])*
        body: { $($body:tt)* }
    ) => {
        $crate::__vld_struct_attrs!(@filter [$($acc)* #[ $($attr)* ]]
            $(#[ $($rest)* ])*
            body: { $($body)* }
        );
    };
    (
        @filter [$($acc:tt)*]
        body: { $($body:tt)* }
    ) => {
        $($acc)*
        $($body)*
    };
}

/// Define a validated struct with field-level schemas.
///
/// This macro generates:
/// - A regular Rust struct with the specified fields and types
/// - A `parse()` method that validates input and constructs the struct
/// - A `parse_value()` method for direct `serde_json::Value` input
/// - An implementation of [`VldParse`](crate::schema::VldParse) for use with framework extractors
///
/// # Syntax
///
/// ```ignore
/// vld::schema! {
///     #[derive(Debug, Clone)]
///     pub struct MyStruct {
///         pub field_name: FieldType => schema_expression,
///         pub renamed_field: FieldType as "jsonKey" => schema_expression,
///         // ...
///     }
/// }
/// ```
///
/// Each field has the format: `name: Type [as "json_key"] => schema`.
/// The optional `as "json_key"` overrides the JSON property name used for parsing.
///
/// # Serde renames
///
/// Without `as "json_key"`, the key follows the serde attributes written in
/// the macro: field `#[serde(rename = "...")]` /
/// `#[serde(rename(serialize = "...", deserialize = "..."))]`, then struct
/// `#[serde(rename_all = "...")]` (also the `serialize` / `deserialize`
/// form; same conventions as serde). `parse()` / `parse_value()` / JSON
/// Schema use the **deserialize** names; `validate()` / `is_valid()`
/// serialize the value and check it under the **serialize** names, so a
/// `Serialize`d instance validates. `as "json_key"` sets both names (serde
/// does not see it: pair it with a matching `#[serde(rename)]` if needed).
/// `alias`, `flatten`, `skip` and `default` are not interpreted.
///
/// # Schema caching
///
/// Every field schema expression is evaluated **once per process** (on first
/// use) and cached in a `static`, so it must be `Send + Sync + 'static`, and
/// a schema built from runtime values (`vld::string().max(config.limit)`)
/// freezes the values seen on the first parse. Build such schemas at runtime
/// with [`vld::object()`](crate::object()) instead.
///
/// # OpenAPI parameters
///
/// Raw identifiers map to their bare name (`r#type` → JSON key `"type"`).
///
/// For query/path/header/cookie parameters, add utoipa's attribute on the struct
/// (same as with `#[derive(Validate)]`), then call `vld_utoipa::impl_to_schema!` once:
///
/// ```ignore
/// vld::schema! {
///     #[derive(Debug)]
///     #[into_params(parameter_in = Query)]
///     pub struct SearchQuery {
///         pub q: String => vld::string().min(1),
///     }
/// }
/// impl_to_schema!(SearchQuery);
/// ```
///
/// # Example
///
/// ```
/// use vld::prelude::*;
///
/// vld::schema! {
///     #[derive(Debug)]
///     pub struct User {
///         pub name: String => vld::string().min(2).max(50),
///         pub age: Option<i64> => vld::number().int().min(0).optional(),
///         pub tags: Vec<String> => vld::array(vld::string()).max_len(5)
///             .with_default(vec![]),
///     }
/// }
///
/// let user = User::parse(r#"{"name": "Alice", "age": 30}"#).unwrap();
/// assert_eq!(user.name, "Alice");
/// assert_eq!(user.age, Some(30));
/// assert!(user.tags.is_empty());
/// ```
///
/// # Renamed Fields
///
/// ```ignore
/// vld::schema! {
///     pub struct ApiResponse {
///         pub first_name: String as "firstName" => vld::string().min(1),
///         pub last_name: String as "lastName" => vld::string().min(1),
///     }
/// }
///
/// // Parses from camelCase JSON:
/// let r = ApiResponse::parse(r#"{"firstName": "John", "lastName": "Doe"}"#).unwrap();
/// assert_eq!(r.first_name, "John");
/// ```
///
/// # Nested Structs
///
/// Use [`nested()`](crate::nested()) to compose schemas:
///
/// ```ignore
/// vld::schema! {
///     pub struct Address {
///         pub city: String => vld::string().min(1),
///     }
/// }
///
/// vld::schema! {
///     pub struct User {
///         pub name: String => vld::string(),
///         pub address: Address => vld::nested(Address::parse_value),
///     }
/// }
/// ```
#[macro_export]
macro_rules! schema {
    (
        $(#[ $($struct_attr:tt)* ])*
        $vis:vis struct $name:ident {
            $(
                $(#[$field_meta:meta])*
                $field_vis:vis $field_name:ident : $field_type:ty $(as $rename:literal)? => $schema:expr
            ),* $(,)?
        }
    ) => {
        $crate::__vld_struct_attrs! {
            attrs: $(#[ $($struct_attr)* ])*
            body: {
        $vis struct $name {
            $(
                $(#[$field_meta])*
                $field_vis $field_name: $field_type,
            )*
        }

        impl $name {
            /// Parse and validate input data into this struct.
            ///
            /// Accepts any type implementing [`VldInput`]: JSON strings, file paths,
            /// `serde_json::Value`, byte slices, etc.
            pub fn parse<__VldInputT: $crate::schema::VldInput + ?Sized>(
                input: &__VldInputT,
            ) -> ::std::result::Result<$name, $crate::error::VldError> {
                let __vld_json = <__VldInputT as $crate::schema::VldInput>::to_json_value(input)?;
                Self::parse_value(&__vld_json)
            }

            /// Parse and validate directly from a `serde_json::Value`.
            pub fn parse_value(
                __vld_json: &$crate::serde_json::Value,
            ) -> ::std::result::Result<$name, $crate::error::VldError> {
                Self::__vld_parse(__vld_json, false)
            }

            /// JSON key of a field: deserialize name (`ser == false`, parsing)
            /// or serialize name (`ser == true`, `validate()`).
            #[doc(hidden)]
            pub fn __vld_key(__vld_field: &str, __vld_ser: bool) -> &'static str {
                static __VLD_KEYS: ::std::sync::OnceLock<
                    ::std::vec::Vec<(&'static str, ::std::string::String, ::std::string::String)>,
                > = ::std::sync::OnceLock::new();
                let __vld_keys = __VLD_KEYS.get_or_init(|| {
                    #[allow(unused_variables)]
                    let __vld_struct_attrs: &[&str] = &[$(stringify!($($struct_attr)*)),*];
                    ::std::vec![$(
                        $crate::__private::serde_keys(
                            stringify!($field_name),
                            $crate::__vld_resolve_key!(None $(, Some($rename))?),
                            &[$(stringify!($field_meta)),*],
                            __vld_struct_attrs,
                        )
                    ),*]
                });
                $crate::__private::key_of(__vld_keys, __vld_field, __vld_ser)
            }

            #[doc(hidden)]
            pub fn __vld_parse(
                __vld_json: &$crate::serde_json::Value,
                __vld_ser: bool,
            ) -> ::std::result::Result<$name, $crate::error::VldError> {
                #[allow(unused_imports)]
                use $crate::schema::VldSchema as _;
                let __vld_obj = $crate::__private::expect_object(__vld_json)?;
                let mut __vld_errors = $crate::error::VldError::new();

                $(
                    #[allow(non_snake_case)]
                    let $field_name: ::std::option::Option<$field_type> =
                        $crate::__private::parse_field(
                            __vld_obj,
                            Self::__vld_key(stringify!($field_name), __vld_ser),
                            $crate::__vld_cached!($schema),
                            &mut __vld_errors,
                        );
                )*

                // A field may fail with an issue-less error: check the values too.
                if !__vld_errors.is_empty() $(|| $field_name.is_none())* {
                    return ::std::result::Result::Err(__vld_errors);
                }

                ::std::result::Result::Ok($name {
                    $(
                        $field_name: $field_name.unwrap(),
                    )*
                })
            }
        }

        impl $crate::schema::VldParse for $name {
            fn vld_parse_value(
                value: &$crate::serde_json::Value,
            ) -> ::std::result::Result<Self, $crate::error::VldError> {
                Self::parse_value(value)
            }
        }

        impl $name {
            /// Validate an existing Rust value that can be serialized to JSON.
            ///
            /// The value is serialized via `serde`, then validated against the
            /// schema under the fields' **serialize** names (`parse()` reads
            /// the deserialize names). Returns `Ok(())` on success,
            /// `Err(VldError)` with all issues on failure.
            pub fn validate<__VldT: $crate::serde::Serialize>(
                instance: &__VldT,
            ) -> ::std::result::Result<(), $crate::error::VldError> {
                Self::__vld_parse(&$crate::__private::to_json(instance)?, true).map(|_| ())
            }

            /// Check if a value is valid against the schema.
            ///
            /// Shorthand for `validate(instance).is_ok()`.
            pub fn is_valid<__VldT: $crate::serde::Serialize>(instance: &__VldT) -> bool {
                Self::validate(instance).is_ok()
            }
        }

        $crate::__vld_if_openapi! {
            impl $name {
                /// Generate a JSON Schema / OpenAPI 3.1 representation of this struct.
                ///
                /// Requires the `openapi` feature.
                pub fn json_schema() -> $crate::serde_json::Value {
                    #[allow(unused_imports)]
                    use $crate::schema::VldSchema as _;
                    #[allow(unused_imports)]
                    use $crate::openapi::JsonSchema as _;
                    static __VLD_JSON_SCHEMA: $crate::__private::JsonSchemaCell =
                        $crate::__private::JsonSchemaCell::new();
                    __VLD_JSON_SCHEMA.get_or_init(|| $crate::__private::object_schema(::std::vec![
                        $(
                            {
                                let __vld_field_schema = $schema;
                                (
                                    Self::__vld_key(stringify!($field_name), false),
                                    __vld_field_schema.json_schema(),
                                    __vld_field_schema.is_required(),
                                )
                            }
                        ),*
                    ])).clone()
                }

                /// Minimal OpenAPI 3.1 document with this struct and every
                /// nested schema it references under `components.schemas`.
                ///
                /// Requires the `openapi` feature.
                pub fn to_openapi_document() -> $crate::serde_json::Value {
                    $crate::__private::openapi_document(
                        stringify!($name),
                        Self::json_schema(),
                        Self::__vld_nested_schemas(),
                    )
                }

                /// Collect `(name, json_schema_fn)` pairs for all nested schemas
                /// used by fields of this struct into `out`.
                ///
                /// Recurses transitively (with name dedup) so grandchild schemas
                /// are registered for OpenAPI components.
                #[doc(hidden)]
                pub fn __vld_collect_nested_schemas(
                    __vld_out: &mut ::std::vec::Vec<$crate::openapi::NestedSchemaEntry>,
                ) {
                    #[allow(unused_imports)]
                    use $crate::schema::VldSchema as _;
                    $(
                        $crate::openapi::CollectNestedSchemas::collect_nested_schemas(
                            &$schema,
                            __vld_out,
                        );
                    )*
                }

                /// Collect `(name, json_schema_fn)` pairs for all nested schemas
                /// used by fields of this struct.
                ///
                /// Used by `vld-utoipa`'s `impl_to_schema!` to automatically register
                /// nested types as OpenAPI components.
                #[doc(hidden)]
                pub fn __vld_nested_schemas()
                    -> ::std::vec::Vec<$crate::openapi::NestedSchemaEntry>
                {
                    let mut __vld_out: ::std::vec::Vec<$crate::openapi::NestedSchemaEntry> =
                        ::std::vec::Vec::new();
                    Self::__vld_collect_nested_schemas(&mut __vld_out);
                    __vld_out
                }
            }

            impl $crate::openapi::OpenApiParameterIn for $name {
                fn parameter_in() -> Option<&'static str> {
                    $crate::schema!(@param_in $(#[ $($struct_attr)* ])*)
                }
            }
        }
            }
        }
    };

    // Parse `#[into_params(..., parameter_in = ..., ...)]` for [`OpenApiParameterIn`].
    (@param_in) => {
        None::<&'static str>
    };
    (@param_in #[ into_params ( $($p:tt)* ) ] $(#[ $($rest:tt)* ])*) => {
        match $crate::schema!(@param_in_args $($p)*) {
            Some(__vld_in) => Some(__vld_in),
            None => $crate::schema!(@param_in $(#[ $($rest)* ])*),
        }
    };
    (@param_in #[ $($skip:tt)* ] $(#[ $($rest:tt)* ])*) => {
        $crate::schema!(@param_in $(#[ $($rest)* ])*)
    };
    // Scan the `into_params(...)` argument list one token tree at a time.
    (@param_in_args) => { None::<&'static str> };
    (@param_in_args parameter_in = Query $($rest:tt)*) => { Some("query") };
    (@param_in_args parameter_in = Path $($rest:tt)*) => { Some("path") };
    (@param_in_args parameter_in = Header $($rest:tt)*) => { Some("header") };
    (@param_in_args parameter_in = Cookie $($rest:tt)*) => { Some("cookie") };
    (@param_in_args $skip:tt $($rest:tt)*) => { $crate::schema!(@param_in_args $($rest)*) };
}

/// Combined macro: generates the struct, `parse()`, **and** `validate_fields()` /
/// `parse_lenient()` in a single declaration — no need to repeat field schemas.
///
/// This is equivalent to calling `schema!` + `impl_validate_fields!` together.
///
/// **Extra requirements** compared to `schema!`:
/// - All field types must implement `serde::Serialize` (for per-field JSON output)
/// - All field types must implement `Default` (for lenient fallback values)
///
/// # Example
///
/// ```ignore
/// vld::schema_validated! {
///     #[derive(Debug, serde::Serialize)]
///     pub struct User {
///         pub name: String => vld::string().min(2),
///         pub age: Option<i64> => vld::number().int().optional(),
///     }
/// }
///
/// // Has parse(), validate_fields(), parse_lenient(), etc.
/// let result = User::parse_lenient(r#"{"name":"X"}"#).unwrap();
/// result.save_to_file(std::path::Path::new("out.json")).unwrap();
/// ```
#[macro_export]
macro_rules! schema_validated {
    (
        $(#[ $($struct_attr:tt)* ])*
        $vis:vis struct $name:ident {
            $(
                $(#[$field_meta:meta])*
                $field_vis:vis $field_name:ident : $field_type:ty $(as $rename:literal)? => $schema:expr
            ),* $(,)?
        }
    ) => {
        // 1. Generate the struct + parse/parse_value (same as schema!)
        $crate::schema! {
            $(#[ $($struct_attr)* ])*
            $vis struct $name {
                $(
                    $(#[$field_meta])*
                    $field_vis $field_name : $field_type $(as $rename)? => $schema
                ),*
            }
        }

        // 2. Generate validate_fields + parse_lenient (same as impl_validate_fields!)
        $crate::impl_validate_fields!(@keyed $name {
            $( $field_name : $field_type => [<$name>::__vld_key(stringify!($field_name), false)] $schema ),*
        });
    };
}

/// Create a named nested schema with `$ref` generation and full JSON Schema.
///
/// Works with types from [`schema!`] and `#[derive(Validate)]`. The component
/// name is the type as written, sanitized to `[A-Za-z0-9._-]`
/// (`Page<User>` → `Page_User_`).
///
/// ```ignore
/// vld::schema! {
///     pub struct User {
///         pub address: Address => vld::nested!(Address),
///     }
/// }
/// ```
#[macro_export]
macro_rules! nested {
    ($ty:ty) => {
        $crate::nested_named(
            {
                static __VLD_NAME: ::std::sync::OnceLock<::std::string::String> =
                    ::std::sync::OnceLock::new();
                __VLD_NAME
                    .get_or_init(|| $crate::__private::component_name(stringify!($ty)))
                    .as_str()
            },
            <$ty>::parse_value,
            $crate::__vld_nested_schema_fn!($ty),
            $crate::__vld_nested_collect_fn!($ty),
        )
    };
}