Skip to main content

vld/
macros.rs

1/// Helper macro to resolve a field's JSON key.
2///
3/// If a rename literal is provided, use it; otherwise fall back to the field name.
4#[doc(hidden)]
5#[macro_export]
6macro_rules! __vld_resolve_key {
7    ($default:expr) => {
8        $default
9    };
10    ($default:expr, $override:expr) => {
11        $override
12    };
13}
14
15/// Define a validated struct with field-level schemas.
16///
17/// This macro generates:
18/// - A regular Rust struct with the specified fields and types
19/// - A `parse()` method that validates input and constructs the struct
20/// - A `parse_value()` method for direct `serde_json::Value` input
21/// - An implementation of [`VldParse`](crate::schema::VldParse) for use with framework extractors
22///
23/// # Syntax
24///
25/// ```ignore
26/// vld::schema! {
27///     #[derive(Debug, Clone)]
28///     pub struct MyStruct {
29///         pub field_name: FieldType => schema_expression,
30///         pub renamed_field: FieldType as "jsonKey" => schema_expression,
31///         // ...
32///     }
33/// }
34/// ```
35///
36/// Each field has the format: `name: Type [as "json_key"] => schema`.
37/// The optional `as "json_key"` overrides the JSON property name used for parsing.
38///
39/// # Example
40///
41/// ```
42/// use vld::prelude::*;
43///
44/// vld::schema! {
45///     #[derive(Debug)]
46///     pub struct User {
47///         pub name: String => vld::string().min(2).max(50),
48///         pub age: Option<i64> => vld::number().int().min(0).optional(),
49///         pub tags: Vec<String> => vld::array(vld::string()).max_len(5)
50///             .with_default(vec![]),
51///     }
52/// }
53///
54/// let user = User::parse(r#"{"name": "Alice", "age": 30}"#).unwrap();
55/// assert_eq!(user.name, "Alice");
56/// assert_eq!(user.age, Some(30));
57/// assert!(user.tags.is_empty());
58/// ```
59///
60/// # Renamed Fields
61///
62/// ```ignore
63/// vld::schema! {
64///     pub struct ApiResponse {
65///         pub first_name: String as "firstName" => vld::string().min(1),
66///         pub last_name: String as "lastName" => vld::string().min(1),
67///     }
68/// }
69///
70/// // Parses from camelCase JSON:
71/// let r = ApiResponse::parse(r#"{"firstName": "John", "lastName": "Doe"}"#).unwrap();
72/// assert_eq!(r.first_name, "John");
73/// ```
74///
75/// # Nested Structs
76///
77/// Use [`nested()`](crate::nested) to compose schemas:
78///
79/// ```ignore
80/// vld::schema! {
81///     pub struct Address {
82///         pub city: String => vld::string().min(1),
83///     }
84/// }
85///
86/// vld::schema! {
87///     pub struct User {
88///         pub name: String => vld::string(),
89///         pub address: Address => vld::nested(Address::parse_value),
90///     }
91/// }
92/// ```
93#[macro_export]
94macro_rules! schema {
95    (
96        $(#[$meta:meta])*
97        $vis:vis struct $name:ident {
98            $(
99                $(#[$field_meta:meta])*
100                $field_vis:vis $field_name:ident : $field_type:ty $(as $rename:literal)? => $schema:expr
101            ),* $(,)?
102        }
103    ) => {
104        $(#[$meta])*
105        $vis struct $name {
106            $(
107                $(#[$field_meta])*
108                $field_vis $field_name: $field_type,
109            )*
110        }
111
112        impl $name {
113            /// Parse and validate input data into this struct.
114            ///
115            /// Accepts any type implementing [`VldInput`]: JSON strings, file paths,
116            /// `serde_json::Value`, byte slices, etc.
117            pub fn parse<__VldInputT: $crate::input::VldInput + ?Sized>(
118                input: &__VldInputT,
119            ) -> ::std::result::Result<$name, $crate::error::VldError> {
120                let __vld_json = <__VldInputT as $crate::input::VldInput>::to_json_value(input)?;
121                Self::parse_value(&__vld_json)
122            }
123
124            /// Parse and validate directly from a `serde_json::Value`.
125            pub fn parse_value(
126                __vld_json: &$crate::serde_json::Value,
127            ) -> ::std::result::Result<$name, $crate::error::VldError> {
128                use $crate::schema::VldSchema as _;
129
130                let __vld_obj = __vld_json.as_object().ok_or_else(|| {
131                    $crate::error::VldError::single(
132                        $crate::error::IssueCode::InvalidType {
133                            expected: ::std::string::String::from("object"),
134                            received: $crate::error::value_type_name(__vld_json),
135                        },
136                        ::std::format!(
137                            "Expected object, received {}",
138                            $crate::error::value_type_name(__vld_json)
139                        ),
140                    )
141                })?;
142
143                let mut __vld_errors = $crate::error::VldError::new();
144
145                $(
146                    #[allow(non_snake_case)]
147                    let $field_name: ::std::option::Option<$field_type> = {
148                        let __vld_field_schema = $schema;
149                        let __vld_key = $crate::__vld_resolve_key!(
150                            stringify!($field_name) $(, $rename)?
151                        );
152                        let __vld_field_value = __vld_obj
153                            .get(__vld_key)
154                            .unwrap_or(&$crate::serde_json::Value::Null);
155                        match __vld_field_schema.parse_value(__vld_field_value) {
156                            ::std::result::Result::Ok(v) => ::std::option::Option::Some(v),
157                            ::std::result::Result::Err(e) => {
158                                __vld_errors = $crate::error::VldError::merge(
159                                    __vld_errors,
160                                    $crate::error::VldError::with_prefix(
161                                        e,
162                                        $crate::error::PathSegment::Field(
163                                            ::std::string::String::from(__vld_key),
164                                        ),
165                                    ),
166                                );
167                                ::std::option::Option::None
168                            }
169                        }
170                    };
171                )*
172
173                if !$crate::error::VldError::is_empty(&__vld_errors) {
174                    return ::std::result::Result::Err(__vld_errors);
175                }
176
177                ::std::result::Result::Ok($name {
178                    $(
179                        $field_name: $field_name.unwrap(),
180                    )*
181                })
182            }
183
184        }
185
186        impl $crate::schema::VldParse for $name {
187            fn vld_parse_value(
188                value: &$crate::serde_json::Value,
189            ) -> ::std::result::Result<Self, $crate::error::VldError> {
190                Self::parse_value(value)
191            }
192        }
193
194
195        $crate::__vld_if_serialize! {
196            impl $name {
197                /// Validate an existing Rust value that can be serialized to JSON.
198                ///
199                /// The value is serialized via `serde`, then validated against the
200                /// schema. Returns `Ok(())` on success, `Err(VldError)` with all
201                /// issues on failure.
202                ///
203                /// Requires the `serialize` feature.
204                pub fn validate<__VldT: $crate::serde::Serialize>(
205                    instance: &__VldT,
206                ) -> ::std::result::Result<(), $crate::error::VldError> {
207                    let __vld_json = $crate::serde_json::to_value(instance).map_err(|e| {
208                        $crate::error::VldError::single(
209                            $crate::error::IssueCode::ParseError,
210                            ::std::format!("Serialization error: {}", e),
211                        )
212                    })?;
213                    let _ = Self::parse_value(&__vld_json)?;
214                    ::std::result::Result::Ok(())
215                }
216
217                /// Check if a value is valid against the schema.
218                ///
219                /// Shorthand for `validate(instance).is_ok()`.
220                ///
221                /// Requires the `serialize` feature.
222                pub fn is_valid<__VldT: $crate::serde::Serialize>(instance: &__VldT) -> bool {
223                    Self::validate(instance).is_ok()
224                }
225            }
226        }
227
228        $crate::__vld_if_openapi! {
229            impl $name {
230                /// Generate a JSON Schema / OpenAPI 3.1 representation of this struct.
231                ///
232                /// Requires the `openapi` feature.
233                pub fn json_schema() -> $crate::serde_json::Value {
234                    use $crate::json_schema::JsonSchema as _;
235                    let mut __vld_properties = $crate::serde_json::Map::new();
236                    let mut __vld_required: ::std::vec::Vec<::std::string::String> =
237                        ::std::vec::Vec::new();
238
239                    $(
240                        {
241                            let __vld_field_schema = $schema;
242                            let __vld_key = $crate::__vld_resolve_key!(
243                                stringify!($field_name) $(, $rename)?
244                            );
245                            __vld_properties.insert(
246                                ::std::string::String::from(__vld_key),
247                                __vld_field_schema.json_schema(),
248                            );
249                            __vld_required.push(
250                                ::std::string::String::from(__vld_key),
251                            );
252                        }
253                    )*
254
255                    $crate::serde_json::json!({
256                        "type": "object",
257                        "required": __vld_required,
258                        "properties": $crate::serde_json::Value::Object(__vld_properties),
259                    })
260                }
261
262                /// Wrap `json_schema()` in a minimal OpenAPI 3.1 document.
263                ///
264                /// Requires the `openapi` feature.
265                pub fn to_openapi_document() -> $crate::serde_json::Value {
266                    $crate::json_schema::to_openapi_document(stringify!($name), &Self::json_schema())
267                }
268
269                /// Collect `(name, json_schema_fn)` pairs for all nested schemas
270                /// used by fields of this struct.
271                ///
272                /// Used by `vld-utoipa`'s `impl_to_schema!` to automatically register
273                /// nested types as OpenAPI components.
274                #[doc(hidden)]
275                pub fn __vld_nested_schemas()
276                    -> ::std::vec::Vec<$crate::json_schema::NestedSchemaEntry>
277                {
278                    use $crate::json_schema::CollectNestedSchemas as _;
279                    let mut __vld_out: ::std::vec::Vec<$crate::json_schema::NestedSchemaEntry> =
280                        ::std::vec::Vec::new();
281
282                    $(
283                        {
284                            let __vld_field_schema = $schema;
285                            __vld_field_schema.collect_nested_schemas(&mut __vld_out);
286                        }
287                    )*
288
289                    __vld_out
290                }
291            }
292        }
293    };
294}
295
296/// Generate `validate_fields()` and `parse_lenient()` methods for a struct
297/// previously defined with [`schema!`].
298///
299/// Syntax mirrors `schema!`, but without visibility/attributes:
300///
301/// ```ignore
302/// vld::impl_validate_fields!(User {
303///     name: String => vld::string().min(2),
304///     age: i64     => vld::number().int(),
305/// });
306/// ```
307///
308/// Fields can also use `as "json_key"` to match a renamed JSON property:
309///
310/// ```ignore
311/// vld::impl_validate_fields!(User {
312///     first_name: String as "firstName" => vld::string().min(2),
313/// });
314/// ```
315///
316/// Generated methods:
317///
318/// - **`validate_fields(input)`** — validate each field, return `Vec<FieldResult>`
319/// - **`parse_lenient(input)`** — build the struct even if some fields fail
320///   (uses `Default` for invalid fields), returns [`ParseResult<Self>`](crate::error::ParseResult)
321///
322/// The returned [`ParseResult`](crate::error::ParseResult) can be inspected,
323/// converted to JSON, or saved to a file at any time via `.save_to_file(path)`.
324///
325/// **Requires:**
326/// - Field output types: `serde::Serialize`
327/// - For `parse_lenient`: field types also need `Default`
328/// - For `save_to_file` / `to_json_string`: the struct needs `serde::Serialize`
329#[macro_export]
330macro_rules! impl_validate_fields {
331    (
332        $name:ident {
333            $( $field_name:ident : $field_type:ty $(as $rename:literal)? => $schema:expr ),* $(,)?
334        }
335    ) => {
336        impl $name {
337            /// Validate each field individually and return per-field results.
338            ///
339            /// Unlike `parse()`, this does **not** fail fast — every field is
340            /// validated and you see which fields passed and which failed.
341            pub fn validate_fields<__VldInputT: $crate::input::VldInput + ?Sized>(
342                input: &__VldInputT,
343            ) -> ::std::result::Result<
344                ::std::vec::Vec<$crate::error::FieldResult>,
345                $crate::error::VldError,
346            > {
347                let __vld_json = <__VldInputT as $crate::input::VldInput>::to_json_value(input)?;
348                Self::validate_fields_value(&__vld_json)
349            }
350
351            /// Validate each field individually from a `serde_json::Value`.
352            pub fn validate_fields_value(
353                __vld_json: &$crate::serde_json::Value,
354            ) -> ::std::result::Result<
355                ::std::vec::Vec<$crate::error::FieldResult>,
356                $crate::error::VldError,
357            > {
358                let __vld_obj = __vld_json.as_object().ok_or_else(|| {
359                    $crate::error::VldError::single(
360                        $crate::error::IssueCode::InvalidType {
361                            expected: ::std::string::String::from("object"),
362                            received: $crate::error::value_type_name(__vld_json),
363                        },
364                        ::std::format!(
365                            "Expected object, received {}",
366                            $crate::error::value_type_name(__vld_json)
367                        ),
368                    )
369                })?;
370
371                let mut __vld_results: ::std::vec::Vec<$crate::error::FieldResult> =
372                    ::std::vec::Vec::new();
373
374                $(
375                    {
376                        let __vld_field_schema = $schema;
377                        let __vld_key = $crate::__vld_resolve_key!(
378                            stringify!($field_name) $(, $rename)?
379                        );
380                        let __vld_field_value = __vld_obj
381                            .get(__vld_key)
382                            .unwrap_or(&$crate::serde_json::Value::Null);
383
384                        let __vld_result = $crate::object::DynSchema::dyn_parse(
385                            &__vld_field_schema,
386                            __vld_field_value,
387                        );
388
389                        __vld_results.push($crate::error::FieldResult {
390                            name: ::std::string::String::from(__vld_key),
391                            input: __vld_field_value.clone(),
392                            result: __vld_result,
393                        });
394                    }
395                )*
396
397                ::std::result::Result::Ok(__vld_results)
398            }
399
400            /// Parse leniently: build the struct even when some fields fail.
401            ///
402            /// - Valid fields get their parsed value.
403            /// - Invalid fields fall back to `Default::default()`.
404            ///
405            /// Returns a [`ParseResult`](crate::error::ParseResult) that wraps
406            /// the struct and per-field diagnostics. You can inspect it, convert
407            /// to JSON, or save to a file whenever you need.
408            pub fn parse_lenient<__VldInputT: $crate::input::VldInput + ?Sized>(
409                input: &__VldInputT,
410            ) -> ::std::result::Result<
411                $crate::error::ParseResult<$name>,
412                $crate::error::VldError,
413            > {
414                let __vld_json = <__VldInputT as $crate::input::VldInput>::to_json_value(input)?;
415                Self::parse_lenient_value(&__vld_json)
416            }
417
418            /// Parse leniently from a `serde_json::Value`.
419            pub fn parse_lenient_value(
420                __vld_json: &$crate::serde_json::Value,
421            ) -> ::std::result::Result<
422                $crate::error::ParseResult<$name>,
423                $crate::error::VldError,
424            > {
425                use $crate::schema::VldSchema as _;
426
427                let __vld_obj = __vld_json.as_object().ok_or_else(|| {
428                    $crate::error::VldError::single(
429                        $crate::error::IssueCode::InvalidType {
430                            expected: ::std::string::String::from("object"),
431                            received: $crate::error::value_type_name(__vld_json),
432                        },
433                        ::std::format!(
434                            "Expected object, received {}",
435                            $crate::error::value_type_name(__vld_json)
436                        ),
437                    )
438                })?;
439
440                let mut __vld_results: ::std::vec::Vec<$crate::error::FieldResult> =
441                    ::std::vec::Vec::new();
442
443                $(
444                    #[allow(non_snake_case)]
445                    let $field_name: $field_type = {
446                        let __vld_field_schema = $schema;
447                        let __vld_key = $crate::__vld_resolve_key!(
448                            stringify!($field_name) $(, $rename)?
449                        );
450                        let __vld_field_value = __vld_obj
451                            .get(__vld_key)
452                            .unwrap_or(&$crate::serde_json::Value::Null);
453
454                        match __vld_field_schema.parse_value(__vld_field_value) {
455                            ::std::result::Result::Ok(v) => {
456                                let __json_repr = $crate::serde_json::to_value(&v)
457                                    .unwrap_or_else(|_| __vld_field_value.clone());
458                                __vld_results.push($crate::error::FieldResult {
459                                    name: ::std::string::String::from(__vld_key),
460                                    input: __vld_field_value.clone(),
461                                    result: ::std::result::Result::Ok(__json_repr),
462                                });
463                                v
464                            }
465                            ::std::result::Result::Err(e) => {
466                                __vld_results.push($crate::error::FieldResult {
467                                    name: ::std::string::String::from(__vld_key),
468                                    input: __vld_field_value.clone(),
469                                    result: ::std::result::Result::Err(e),
470                                });
471                                <$field_type as ::std::default::Default>::default()
472                            }
473                        }
474                    };
475                )*
476
477                let __vld_struct = $name {
478                    $( $field_name, )*
479                };
480
481                ::std::result::Result::Ok(
482                    $crate::error::ParseResult::new(__vld_struct, __vld_results)
483                )
484            }
485        }
486    };
487}
488
489/// Combined macro: generates the struct, `parse()`, **and** `validate_fields()` /
490/// `parse_lenient()` in a single declaration — no need to repeat field schemas.
491///
492/// This is equivalent to calling `schema!` + `impl_validate_fields!` together.
493///
494/// **Extra requirements** compared to `schema!`:
495/// - All field types must implement `serde::Serialize` (for per-field JSON output)
496/// - All field types must implement `Default` (for lenient fallback values)
497///
498/// # Example
499///
500/// ```ignore
501/// vld::schema_validated! {
502///     #[derive(Debug, serde::Serialize)]
503///     pub struct User {
504///         pub name: String => vld::string().min(2),
505///         pub age: Option<i64> => vld::number().int().optional(),
506///     }
507/// }
508///
509/// // Has parse(), validate_fields(), parse_lenient(), etc.
510/// let result = User::parse_lenient(r#"{"name":"X"}"#).unwrap();
511/// result.save_to_file(std::path::Path::new("out.json")).unwrap();
512/// ```
513#[macro_export]
514macro_rules! schema_validated {
515    (
516        $(#[$meta:meta])*
517        $vis:vis struct $name:ident {
518            $(
519                $(#[$field_meta:meta])*
520                $field_vis:vis $field_name:ident : $field_type:ty $(as $rename:literal)? => $schema:expr
521            ),* $(,)?
522        }
523    ) => {
524        // 1. Generate the struct + parse/parse_value (same as schema!)
525        $crate::schema! {
526            $(#[$meta])*
527            $vis struct $name {
528                $(
529                    $(#[$field_meta])*
530                    $field_vis $field_name : $field_type $(as $rename)? => $schema
531                ),*
532            }
533        }
534
535        // 2. Generate validate_fields + parse_lenient (same as impl_validate_fields!)
536        $crate::impl_validate_fields!($name {
537            $( $field_name : $field_type $(as $rename)? => $schema ),*
538        });
539    };
540}
541
542/// Attach validation rules to an **existing** struct.
543///
544/// Unlike [`schema!`] which creates the struct, this macro takes a struct you
545/// already have and generates `validate()` and `is_valid()` instance methods.
546///
547/// The struct must implement `serde::Serialize`.
548///
549/// The struct does **not** need `#[derive(Serialize)]` or `#[derive(Debug)]` —
550/// each field is serialized individually (standard types like `String`, `f64`,
551/// `Vec<T>` already implement `Serialize`).
552///
553/// # Example
554///
555/// ```
556/// use vld::prelude::*;
557///
558/// // No Serialize or Debug required on the struct itself
559/// struct Product {
560///     name: String,
561///     price: f64,
562///     tags: Vec<String>,
563/// }
564///
565/// vld::impl_rules!(Product {
566///     name => vld::string().min(2).max(100),
567///     price => vld::number().positive(),
568///     tags => vld::array(vld::string().min(1)).max_len(10),
569/// });
570///
571/// let p = Product {
572///     name: "Widget".into(),
573///     price: 9.99,
574///     tags: vec!["sale".into()],
575/// };
576/// assert!(p.is_valid());
577///
578/// let bad = Product {
579///     name: "X".into(),
580///     price: -1.0,
581///     tags: vec![],
582/// };
583/// assert!(!bad.is_valid());
584/// let err = bad.validate().unwrap_err();
585/// assert!(err.issues.len() >= 2);
586/// ```
587#[macro_export]
588macro_rules! impl_rules {
589    (
590        $name:ident {
591            $( $field:ident => $schema:expr ),* $(,)?
592        }
593    ) => {
594        impl $name {
595            /// Validate this instance against the declared rules.
596            ///
597            /// Each field is serialized to JSON individually and checked
598            /// against its schema. All errors are accumulated.
599            pub fn validate(&self) -> ::std::result::Result<(), $crate::error::VldError> {
600                use $crate::schema::VldSchema as _;
601                let mut __vld_errors = $crate::error::VldError::new();
602
603                $(
604                    {
605                        let __vld_field_json = $crate::serde_json::to_value(&self.$field)
606                            .map_err(|e| {
607                                $crate::error::VldError::single(
608                                    $crate::error::IssueCode::ParseError,
609                                    ::std::format!(
610                                        "Serialization error for field '{}': {}",
611                                        stringify!($field), e
612                                    ),
613                                )
614                            });
615                        match __vld_field_json {
616                            ::std::result::Result::Ok(ref __vld_val) => {
617                                let __vld_field_schema = $schema;
618                                if let ::std::result::Result::Err(e) =
619                                    __vld_field_schema.parse_value(__vld_val)
620                                {
621                                    __vld_errors = $crate::error::VldError::merge(
622                                        __vld_errors,
623                                        $crate::error::VldError::with_prefix(
624                                            e,
625                                            $crate::error::PathSegment::Field(
626                                                ::std::string::String::from(stringify!($field)),
627                                            ),
628                                        ),
629                                    );
630                                }
631                            }
632                            ::std::result::Result::Err(e) => {
633                                __vld_errors = $crate::error::VldError::merge(
634                                    __vld_errors,
635                                    $crate::error::VldError::with_prefix(
636                                        e,
637                                        $crate::error::PathSegment::Field(
638                                            ::std::string::String::from(stringify!($field)),
639                                        ),
640                                    ),
641                                );
642                            }
643                        }
644                    }
645                )*
646
647                if __vld_errors.is_empty() {
648                    ::std::result::Result::Ok(())
649                } else {
650                    ::std::result::Result::Err(__vld_errors)
651                }
652            }
653
654            /// Check if this instance passes all validation rules.
655            pub fn is_valid(&self) -> bool {
656                self.validate().is_ok()
657            }
658        }
659    };
660}
661
662/// Generate `impl Default` for a struct created by [`schema!`].
663///
664/// Use this instead of `#[derive(Default)]` to automatically generate a
665/// `Default` implementation bounded on all field types implementing `Default`.
666///
667/// # Example
668///
669/// ```
670/// use vld::prelude::*;
671///
672/// vld::schema! {
673///     #[derive(Debug)]
674///     pub struct Config {
675///         pub host: String => vld::string().with_default("localhost".into()),
676///         pub port: Option<i64> => vld::number().int().optional(),
677///         pub tags: Vec<String> => vld::array(vld::string()).with_default(vec![]),
678///     }
679/// }
680///
681/// vld::impl_default!(Config { host, port, tags });
682///
683/// let cfg = Config::default();
684/// assert_eq!(cfg.host, "");       // String::default()
685/// assert_eq!(cfg.port, None);     // Option::default()
686/// assert!(cfg.tags.is_empty());   // Vec::default()
687/// ```
688#[macro_export]
689macro_rules! impl_default {
690    ($name:ident { $($field:ident),* $(,)? }) => {
691        impl ::std::default::Default for $name {
692            fn default() -> Self {
693                Self {
694                    $( $field: ::std::default::Default::default(), )*
695                }
696            }
697        }
698    };
699}
700
701/// Create a union schema from 2 or more schemas.
702///
703/// Dispatches to `vld::union()` for 2 schemas and `vld::union3()` for 3.
704/// For 4+ schemas, unions are nested automatically.
705///
706/// # Examples
707///
708/// ```rust
709/// use vld::prelude::*;
710///
711/// // 2 schemas
712/// let s = vld::union!(vld::string(), vld::number().int());
713/// assert!(s.parse(r#""hello""#).is_ok());
714/// assert!(s.parse("42").is_ok());
715///
716/// // 3 schemas
717/// let s = vld::union!(vld::string(), vld::number().int(), vld::boolean());
718/// assert!(s.parse("true").is_ok());
719/// ```
720#[macro_export]
721macro_rules! union {
722    // 2 schemas
723    ($a:expr, $b:expr $(,)?) => {
724        $crate::union($a, $b)
725    };
726    // 3 schemas
727    ($a:expr, $b:expr, $c:expr $(,)?) => {
728        $crate::union3($a, $b, $c)
729    };
730    // 4 schemas — nest as union(union(a, b), union(c, d))
731    ($a:expr, $b:expr, $c:expr, $d:expr $(,)?) => {
732        $crate::union($crate::union($a, $b), $crate::union($c, $d))
733    };
734    // 5 schemas
735    ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr $(,)?) => {
736        $crate::union($crate::union3($a, $b, $c), $crate::union($d, $e))
737    };
738    // 6 schemas
739    ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr $(,)?) => {
740        $crate::union($crate::union3($a, $b, $c), $crate::union3($d, $e, $f))
741    };
742}