Skip to main content

pjson_rs/application/dto/
schema_dto.rs

1//! Schema Data Transfer Objects
2//!
3//! DTOs for transferring schema data across application boundaries.
4
5use pjson_rs_domain::value_objects::enter_deserialize_depth;
6use serde::{Deserialize, Deserializer, Serialize};
7use std::cell::Cell;
8use std::collections::HashMap;
9
10use crate::domain::value_objects::Schema;
11
12thread_local! {
13    /// Per-thread nesting depth reached while deserializing [`SchemaDefinitionDto`].
14    ///
15    /// `SchemaDefinitionDto` is deserialized directly from untrusted input via
16    /// `#[derive(Deserialize)]`, and its recursive conversion into
17    /// [`Schema`] (`impl From<SchemaDefinitionDto> for Schema`) only runs
18    /// *after* deserialization has already fully materialized the DTO tree —
19    /// so `Schema`'s own depth guard (`crates/pjs-domain/src/value_objects/schema.rs`)
20    /// never gets a chance to bound the nesting reached while deserializing
21    /// the DTO itself. Without this guard, a deeply nested
22    /// `SchemaDefinitionDto` document (`items`, `properties`,
23    /// `OneOf`/`AllOf::schemas`) exhausts the stack (CWE-674) before the
24    /// `From` conversion is ever reached.
25    ///
26    /// This mirrors `Schema`'s guard exactly, sharing the same
27    /// `pjson_rs_domain::value_objects::enter_deserialize_depth` primitive
28    /// (thread-local counter + `deserialize_with`, reusing
29    /// `pjson_rs_domain::MAX_DESERIALIZE_DEPTH`) and requires
30    /// `SchemaDefinitionDto` to use serde's default externally-tagged
31    /// representation: an internally-tagged enum (`#[serde(tag = "...")]`)
32    /// buffers the whole nested value into a generic `Content` tree to find
33    /// the tag *before* any field's `deserialize_with` hook runs, so for a
34    /// self-describing format with no recursion limit of its own (e.g.
35    /// MessagePack), sufficiently deep input would overflow the stack during
36    /// that buffering pass regardless of this guard.
37    static SCHEMA_DTO_DESERIALIZE_DEPTH: Cell<usize> = const { Cell::new(0) };
38}
39
40/// Bounded-depth `deserialize_with` for `SchemaDefinitionDto::Array`'s `items` field.
41fn deserialize_boxed_schema_dto_option<'de, D>(
42    deserializer: D,
43) -> Result<Option<Box<SchemaDefinitionDto>>, D::Error>
44where
45    D: Deserializer<'de>,
46{
47    let _guard =
48        enter_deserialize_depth::<D::Error>(&SCHEMA_DTO_DESERIALIZE_DEPTH, "SchemaDefinitionDto")?;
49    Option::<Box<SchemaDefinitionDto>>::deserialize(deserializer)
50}
51
52/// Bounded-depth `deserialize_with` for `SchemaDefinitionDto::Object`'s `properties` field.
53fn deserialize_schema_dto_properties<'de, D>(
54    deserializer: D,
55) -> Result<HashMap<String, SchemaDefinitionDto>, D::Error>
56where
57    D: Deserializer<'de>,
58{
59    let _guard =
60        enter_deserialize_depth::<D::Error>(&SCHEMA_DTO_DESERIALIZE_DEPTH, "SchemaDefinitionDto")?;
61    HashMap::<String, SchemaDefinitionDto>::deserialize(deserializer)
62}
63
64/// Bounded-depth `deserialize_with` for `SchemaDefinitionDto::OneOf`/`AllOf`'s `schemas` field.
65fn deserialize_schema_dto_list<'de, D>(
66    deserializer: D,
67) -> Result<Vec<SchemaDefinitionDto>, D::Error>
68where
69    D: Deserializer<'de>,
70{
71    let _guard =
72        enter_deserialize_depth::<D::Error>(&SCHEMA_DTO_DESERIALIZE_DEPTH, "SchemaDefinitionDto")?;
73    Vec::<SchemaDefinitionDto>::deserialize(deserializer)
74}
75
76/// Schema registration DTO
77///
78/// Used when registering a new schema in the system.
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct SchemaRegistrationDto {
81    /// Unique schema identifier
82    pub id: String,
83    /// Schema definition
84    pub schema: SchemaDefinitionDto,
85    /// Optional schema metadata
86    pub metadata: Option<SchemaMetadataDto>,
87}
88
89/// Schema metadata DTO
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct SchemaMetadataDto {
92    /// Schema version
93    pub version: String,
94    /// Schema description
95    pub description: Option<String>,
96    /// Schema author
97    pub author: Option<String>,
98    /// Creation timestamp
99    pub created_at: Option<i64>,
100}
101
102/// Schema definition DTO
103///
104/// Simplified JSON-serializable representation of schema.
105///
106/// Uses serde's default externally-tagged representation (e.g.
107/// `{"Object": {"properties": {...}, ...}}` rather than
108/// `{"type": "object", "properties": {...}}`) so that the recursive fields'
109/// deserialization depth guard (see the module-private `SCHEMA_DTO_DESERIALIZE_DEPTH` thread-local) can
110/// actually bound nesting — an internally-tagged representation would defeat
111/// it, see that guard's doc comment for why.
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub enum SchemaDefinitionDto {
114    /// JSON string with optional length, pattern, and enumeration constraints.
115    String {
116        /// Minimum allowed length in UTF-8 bytes.
117        #[serde(skip_serializing_if = "Option::is_none")]
118        min_length: Option<usize>,
119        /// Maximum allowed length in UTF-8 bytes.
120        #[serde(skip_serializing_if = "Option::is_none")]
121        max_length: Option<usize>,
122        /// Regular expression the string must match.
123        #[serde(skip_serializing_if = "Option::is_none")]
124        pattern: Option<String>,
125        /// Closed set of values the string is permitted to take.
126        #[serde(skip_serializing_if = "Option::is_none")]
127        enum_values: Option<Vec<String>>,
128    },
129    /// JSON integer with optional inclusive bounds.
130    Integer {
131        /// Minimum allowed value (inclusive).
132        #[serde(skip_serializing_if = "Option::is_none")]
133        minimum: Option<i64>,
134        /// Maximum allowed value (inclusive).
135        #[serde(skip_serializing_if = "Option::is_none")]
136        maximum: Option<i64>,
137    },
138    /// JSON number (floating-point) with optional inclusive bounds.
139    Number {
140        /// Minimum allowed value (inclusive).
141        #[serde(skip_serializing_if = "Option::is_none")]
142        minimum: Option<f64>,
143        /// Maximum allowed value (inclusive).
144        #[serde(skip_serializing_if = "Option::is_none")]
145        maximum: Option<f64>,
146    },
147    /// JSON boolean.
148    Boolean,
149    /// JSON null.
150    Null,
151    /// JSON array with optional element schema and size constraints.
152    Array {
153        /// Schema each element must satisfy, or `None` to allow any.
154        #[serde(
155            default,
156            skip_serializing_if = "Option::is_none",
157            deserialize_with = "deserialize_boxed_schema_dto_option"
158        )]
159        items: Option<Box<SchemaDefinitionDto>>,
160        /// Minimum number of elements (inclusive).
161        #[serde(skip_serializing_if = "Option::is_none")]
162        min_items: Option<usize>,
163        /// Maximum number of elements (inclusive).
164        #[serde(skip_serializing_if = "Option::is_none")]
165        max_items: Option<usize>,
166        /// When `true`, all elements must be distinct.
167        #[serde(default)]
168        unique_items: bool,
169    },
170    /// JSON object with named properties and required-field constraints.
171    Object {
172        /// Schema of each named property.
173        #[serde(deserialize_with = "deserialize_schema_dto_properties")]
174        properties: HashMap<String, SchemaDefinitionDto>,
175        /// Names of properties that must be present.
176        #[serde(default)]
177        required: Vec<String>,
178        /// When `true`, properties not listed in `properties` are accepted.
179        #[serde(default = "default_true")]
180        additional_properties: bool,
181    },
182    /// Logical OR — value must validate against exactly one branch.
183    OneOf {
184        /// Candidate schemas; the value must match exactly one.
185        #[serde(deserialize_with = "deserialize_schema_dto_list")]
186        schemas: Vec<SchemaDefinitionDto>,
187    },
188    /// Logical AND — value must validate against every branch.
189    AllOf {
190        /// Schemas all of which the value must satisfy.
191        #[serde(deserialize_with = "deserialize_schema_dto_list")]
192        schemas: Vec<SchemaDefinitionDto>,
193    },
194    /// Accepts any JSON value without further validation.
195    Any,
196}
197
198fn default_true() -> bool {
199    true
200}
201
202impl From<SchemaDefinitionDto> for Schema {
203    fn from(dto: SchemaDefinitionDto) -> Self {
204        match dto {
205            SchemaDefinitionDto::String {
206                min_length,
207                max_length,
208                pattern,
209                enum_values,
210            } => Self::String {
211                min_length,
212                max_length,
213                pattern,
214                allowed_values: enum_values
215                    .map(|values| values.into_iter().collect::<smallvec::SmallVec<[_; 8]>>()),
216            },
217            SchemaDefinitionDto::Integer { minimum, maximum } => Self::Integer { minimum, maximum },
218            SchemaDefinitionDto::Number { minimum, maximum } => Self::Number { minimum, maximum },
219            SchemaDefinitionDto::Boolean => Self::Boolean,
220            SchemaDefinitionDto::Null => Self::Null,
221            SchemaDefinitionDto::Array {
222                items,
223                min_items,
224                max_items,
225                unique_items,
226            } => Self::Array {
227                items: items.map(|i| Box::new((*i).into())),
228                min_items,
229                max_items,
230                unique_items,
231            },
232            SchemaDefinitionDto::Object {
233                properties,
234                required,
235                additional_properties,
236            } => Self::Object {
237                properties: properties.into_iter().map(|(k, v)| (k, v.into())).collect(),
238                required,
239                additional_properties,
240            },
241            SchemaDefinitionDto::OneOf { schemas } => Self::OneOf {
242                schemas: schemas
243                    .into_iter()
244                    .map(|s| Box::new(s.into()))
245                    .collect::<smallvec::SmallVec<[_; 4]>>(),
246            },
247            SchemaDefinitionDto::AllOf { schemas } => Self::AllOf {
248                schemas: schemas
249                    .into_iter()
250                    .map(|s| Box::new(s.into()))
251                    .collect::<smallvec::SmallVec<[_; 4]>>(),
252            },
253            SchemaDefinitionDto::Any => Self::Any,
254        }
255    }
256}
257
258impl From<&Schema> for SchemaDefinitionDto {
259    fn from(schema: &Schema) -> Self {
260        match schema {
261            Schema::String {
262                min_length,
263                max_length,
264                pattern,
265                allowed_values,
266            } => Self::String {
267                min_length: *min_length,
268                max_length: *max_length,
269                pattern: pattern.as_ref().map(|p| p.to_string()),
270                enum_values: allowed_values
271                    .as_ref()
272                    .map(|v| v.iter().map(|s| s.to_string()).collect()),
273            },
274            Schema::Integer { minimum, maximum } => Self::Integer {
275                minimum: *minimum,
276                maximum: *maximum,
277            },
278            Schema::Number { minimum, maximum } => Self::Number {
279                minimum: *minimum,
280                maximum: *maximum,
281            },
282            Schema::Boolean => Self::Boolean,
283            Schema::Null => Self::Null,
284            Schema::Array {
285                items,
286                min_items,
287                max_items,
288                unique_items,
289            } => Self::Array {
290                items: items.as_ref().map(|i| Box::new(i.as_ref().into())),
291                min_items: *min_items,
292                max_items: *max_items,
293                unique_items: *unique_items,
294            },
295            Schema::Object {
296                properties,
297                required,
298                additional_properties,
299            } => Self::Object {
300                properties: properties
301                    .iter()
302                    .map(|(k, v)| (k.clone(), v.into()))
303                    .collect(),
304                required: required.clone(),
305                additional_properties: *additional_properties,
306            },
307            Schema::OneOf { schemas } => Self::OneOf {
308                schemas: schemas.iter().map(|s| s.as_ref().into()).collect(),
309            },
310            Schema::AllOf { schemas } => Self::AllOf {
311                schemas: schemas.iter().map(|s| s.as_ref().into()).collect(),
312            },
313            Schema::Any => Self::Any,
314            _ => Self::Any,
315        }
316    }
317}
318
319/// Validation request DTO
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct ValidationRequestDto {
322    /// Schema ID to validate against
323    pub schema_id: String,
324    /// JSON data to validate (as string)
325    pub data: String,
326}
327
328/// Validation result DTO
329#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct ValidationResultDto {
331    /// Whether validation succeeded
332    pub valid: bool,
333    /// Validation errors (if any)
334    #[serde(skip_serializing_if = "Vec::is_empty")]
335    pub errors: Vec<ValidationErrorDto>,
336}
337
338/// Validation error DTO
339#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct ValidationErrorDto {
341    /// JSON path where error occurred
342    pub path: String,
343    /// Error message
344    pub message: String,
345    /// Error type
346    pub error_type: String,
347}
348
349impl From<&crate::domain::value_objects::SchemaValidationError> for ValidationErrorDto {
350    fn from(error: &crate::domain::value_objects::SchemaValidationError) -> Self {
351        use crate::domain::value_objects::SchemaValidationError;
352
353        let (error_type, path, message) = match error {
354            SchemaValidationError::TypeMismatch {
355                path,
356                expected,
357                actual,
358            } => (
359                "type_mismatch".to_string(),
360                path.clone(),
361                format!("Expected {expected}, got {actual}"),
362            ),
363            SchemaValidationError::MissingRequired { path, field } => (
364                "missing_required".to_string(),
365                path.clone(),
366                format!("Missing required field: {field}"),
367            ),
368            SchemaValidationError::OutOfRange {
369                path,
370                value,
371                min,
372                max,
373            } => (
374                "out_of_range".to_string(),
375                path.clone(),
376                format!("Value {value} not in range [{min}, {max}]"),
377            ),
378            SchemaValidationError::StringLengthConstraint {
379                path,
380                actual,
381                min,
382                max,
383            } => (
384                "string_length".to_string(),
385                path.clone(),
386                format!("String length {actual} not in range [{min}, {max}]"),
387            ),
388            SchemaValidationError::PatternMismatch {
389                path,
390                value,
391                pattern,
392            } => (
393                "pattern_mismatch".to_string(),
394                path.clone(),
395                format!("Value '{value}' does not match pattern '{pattern}'"),
396            ),
397            SchemaValidationError::ArraySizeConstraint {
398                path,
399                actual,
400                min,
401                max,
402            } => (
403                "array_size".to_string(),
404                path.clone(),
405                format!("Array size {actual} not in range [{min}, {max}]"),
406            ),
407            SchemaValidationError::DuplicateItems { path } => (
408                "duplicate_items".to_string(),
409                path.clone(),
410                "Array contains duplicate items".to_string(),
411            ),
412            SchemaValidationError::InvalidEnumValue { path, value } => (
413                "invalid_enum".to_string(),
414                path.clone(),
415                format!("Value '{value}' not in allowed values"),
416            ),
417            SchemaValidationError::AdditionalPropertyNotAllowed { path, property } => (
418                "additional_property".to_string(),
419                path.clone(),
420                format!("Additional property '{property}' not allowed"),
421            ),
422            SchemaValidationError::NoMatchingOneOf { path } => (
423                "no_matching_one_of".to_string(),
424                path.clone(),
425                "No matching schema in OneOf".to_string(),
426            ),
427            SchemaValidationError::AllOfFailure { path, failures } => (
428                "all_of_failure".to_string(),
429                path.clone(),
430                format!("AllOf validation failed for schemas: {failures}"),
431            ),
432            SchemaValidationError::InvalidPattern {
433                path,
434                pattern,
435                reason,
436            } => (
437                "invalid_pattern".to_string(),
438                path.clone(),
439                format!("Pattern '{pattern}' is not valid regex: {reason}"),
440            ),
441            _ => ("unknown".to_string(), String::new(), error.to_string()),
442        };
443
444        Self {
445            path,
446            message,
447            error_type,
448        }
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455    use crate::domain::value_objects::{Schema, SchemaValidationError};
456
457    // ===========================================
458    // SchemaDefinitionDto Serialization Tests
459    // ===========================================
460
461    #[test]
462    fn test_schema_dto_string_serialization() {
463        let dto = SchemaDefinitionDto::String {
464            min_length: Some(1),
465            max_length: Some(100),
466            pattern: Some("^[a-z]+$".to_string()),
467            enum_values: Some(vec!["hello".to_string(), "world".to_string()]),
468        };
469
470        let json = serde_json::to_string(&dto).unwrap();
471        let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
472
473        assert!(matches!(
474            deserialized,
475            SchemaDefinitionDto::String {
476                min_length: Some(1),
477                max_length: Some(100),
478                pattern: Some(_),
479                enum_values: Some(_)
480            }
481        ));
482    }
483
484    #[test]
485    fn test_schema_dto_string_minimal_serialization() {
486        let dto = SchemaDefinitionDto::String {
487            min_length: None,
488            max_length: None,
489            pattern: None,
490            enum_values: None,
491        };
492
493        let json = serde_json::to_string(&dto).unwrap();
494        let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
495
496        assert!(matches!(
497            deserialized,
498            SchemaDefinitionDto::String {
499                min_length: None,
500                max_length: None,
501                pattern: None,
502                enum_values: None
503            }
504        ));
505    }
506
507    #[test]
508    fn test_schema_dto_integer_serialization() {
509        let dto = SchemaDefinitionDto::Integer {
510            minimum: Some(-100),
511            maximum: Some(100),
512        };
513
514        let json = serde_json::to_string(&dto).unwrap();
515        let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
516
517        assert!(matches!(
518            deserialized,
519            SchemaDefinitionDto::Integer {
520                minimum: Some(-100),
521                maximum: Some(100)
522            }
523        ));
524    }
525
526    #[test]
527    fn test_schema_dto_number_serialization() {
528        let dto = SchemaDefinitionDto::Number {
529            minimum: Some(0.5),
530            maximum: Some(99.9),
531        };
532
533        let json = serde_json::to_string(&dto).unwrap();
534        let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
535
536        assert!(matches!(deserialized, SchemaDefinitionDto::Number { .. }));
537    }
538
539    #[test]
540    fn test_schema_dto_boolean_serialization() {
541        let dto = SchemaDefinitionDto::Boolean;
542
543        let json = serde_json::to_string(&dto).unwrap();
544        let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
545
546        assert!(matches!(deserialized, SchemaDefinitionDto::Boolean));
547    }
548
549    #[test]
550    fn test_schema_dto_null_serialization() {
551        let dto = SchemaDefinitionDto::Null;
552
553        let json = serde_json::to_string(&dto).unwrap();
554        let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
555
556        assert!(matches!(deserialized, SchemaDefinitionDto::Null));
557    }
558
559    #[test]
560    fn test_schema_dto_array_serialization() {
561        let dto = SchemaDefinitionDto::Array {
562            items: Some(Box::new(SchemaDefinitionDto::String {
563                min_length: None,
564                max_length: None,
565                pattern: None,
566                enum_values: None,
567            })),
568            min_items: Some(1),
569            max_items: Some(10),
570            unique_items: true,
571        };
572
573        let json = serde_json::to_string(&dto).unwrap();
574        let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
575
576        assert!(matches!(
577            deserialized,
578            SchemaDefinitionDto::Array {
579                items: Some(_),
580                min_items: Some(1),
581                max_items: Some(10),
582                unique_items: true
583            }
584        ));
585    }
586
587    #[test]
588    fn test_schema_dto_array_minimal_serialization() {
589        let dto = SchemaDefinitionDto::Array {
590            items: None,
591            min_items: None,
592            max_items: None,
593            unique_items: false,
594        };
595
596        let json = serde_json::to_string(&dto).unwrap();
597        let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
598
599        assert!(matches!(
600            deserialized,
601            SchemaDefinitionDto::Array {
602                items: None,
603                min_items: None,
604                max_items: None,
605                unique_items: false
606            }
607        ));
608    }
609
610    #[test]
611    fn test_schema_dto_object_serialization() {
612        let mut properties = HashMap::new();
613        properties.insert(
614            "name".to_string(),
615            SchemaDefinitionDto::String {
616                min_length: Some(1),
617                max_length: None,
618                pattern: None,
619                enum_values: None,
620            },
621        );
622        properties.insert(
623            "age".to_string(),
624            SchemaDefinitionDto::Integer {
625                minimum: Some(0),
626                maximum: Some(150),
627            },
628        );
629
630        let dto = SchemaDefinitionDto::Object {
631            properties,
632            required: vec!["name".to_string()],
633            additional_properties: false,
634        };
635
636        let json = serde_json::to_string(&dto).unwrap();
637        let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
638
639        assert!(matches!(deserialized, SchemaDefinitionDto::Object {
640            properties,
641            required,
642            additional_properties: false
643        } if properties.len() == 2 && required.len() == 1));
644    }
645
646    #[test]
647    fn test_schema_dto_object_allow_additional_properties() {
648        let properties = HashMap::new();
649        let dto = SchemaDefinitionDto::Object {
650            properties,
651            required: vec![],
652            additional_properties: true,
653        };
654
655        let json = serde_json::to_string(&dto).unwrap();
656        let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
657
658        assert!(matches!(
659            deserialized,
660            SchemaDefinitionDto::Object {
661                additional_properties: true,
662                ..
663            }
664        ));
665    }
666
667    #[test]
668    fn test_schema_dto_oneof_serialization() {
669        let dto = SchemaDefinitionDto::OneOf {
670            schemas: vec![
671                SchemaDefinitionDto::String {
672                    min_length: None,
673                    max_length: None,
674                    pattern: None,
675                    enum_values: None,
676                },
677                SchemaDefinitionDto::Integer {
678                    minimum: None,
679                    maximum: None,
680                },
681            ],
682        };
683
684        let json = serde_json::to_string(&dto).unwrap();
685        let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
686
687        assert!(
688            matches!(deserialized, SchemaDefinitionDto::OneOf { schemas } if schemas.len() == 2)
689        );
690    }
691
692    #[test]
693    fn test_schema_dto_allof_serialization() {
694        let dto = SchemaDefinitionDto::AllOf {
695            schemas: vec![
696                SchemaDefinitionDto::String {
697                    min_length: Some(1),
698                    max_length: None,
699                    pattern: None,
700                    enum_values: None,
701                },
702                SchemaDefinitionDto::String {
703                    min_length: None,
704                    max_length: Some(100),
705                    pattern: None,
706                    enum_values: None,
707                },
708            ],
709        };
710
711        let json = serde_json::to_string(&dto).unwrap();
712        let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
713
714        assert!(
715            matches!(deserialized, SchemaDefinitionDto::AllOf { schemas } if schemas.len() == 2)
716        );
717    }
718
719    #[test]
720    fn test_schema_dto_any_serialization() {
721        let dto = SchemaDefinitionDto::Any;
722
723        let json = serde_json::to_string(&dto).unwrap();
724        let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
725
726        assert!(matches!(deserialized, SchemaDefinitionDto::Any));
727    }
728
729    // ===========================================
730    // DTO to Domain Schema Conversion Tests
731    // ===========================================
732
733    #[test]
734    fn test_schema_dto_to_domain_string() {
735        let dto = SchemaDefinitionDto::String {
736            min_length: Some(5),
737            max_length: Some(50),
738            pattern: Some("[a-z]+".to_string()),
739            enum_values: Some(vec!["foo".to_string(), "bar".to_string()]),
740        };
741
742        let schema: Schema = dto.into();
743
744        assert!(matches!(
745            schema,
746            Schema::String {
747                min_length: Some(5),
748                max_length: Some(50),
749                pattern: Some(_),
750                allowed_values: Some(_)
751            }
752        ));
753    }
754
755    #[test]
756    fn test_schema_dto_to_domain_integer() {
757        let dto = SchemaDefinitionDto::Integer {
758            minimum: Some(10),
759            maximum: Some(20),
760        };
761
762        let schema: Schema = dto.into();
763
764        assert!(matches!(
765            schema,
766            Schema::Integer {
767                minimum: Some(10),
768                maximum: Some(20)
769            }
770        ));
771    }
772
773    #[test]
774    fn test_schema_dto_to_domain_number() {
775        let dto = SchemaDefinitionDto::Number {
776            minimum: Some(1.5),
777            maximum: Some(9.9),
778        };
779
780        let schema: Schema = dto.into();
781
782        assert!(matches!(schema, Schema::Number {
783            minimum: Some(min),
784            maximum: Some(max)
785        } if (min - 1.5).abs() < 0.001 && (max - 9.9).abs() < 0.001));
786    }
787
788    #[test]
789    fn test_schema_dto_to_domain_boolean() {
790        let dto = SchemaDefinitionDto::Boolean;
791        let schema: Schema = dto.into();
792
793        assert!(matches!(schema, Schema::Boolean));
794    }
795
796    #[test]
797    fn test_schema_dto_to_domain_null() {
798        let dto = SchemaDefinitionDto::Null;
799        let schema: Schema = dto.into();
800
801        assert!(matches!(schema, Schema::Null));
802    }
803
804    #[test]
805    fn test_schema_dto_to_domain_array_with_items() {
806        let dto = SchemaDefinitionDto::Array {
807            items: Some(Box::new(SchemaDefinitionDto::Integer {
808                minimum: None,
809                maximum: None,
810            })),
811            min_items: Some(0),
812            max_items: Some(100),
813            unique_items: true,
814        };
815
816        let schema: Schema = dto.into();
817
818        assert!(matches!(
819            schema,
820            Schema::Array {
821                items: Some(_),
822                min_items: Some(0),
823                max_items: Some(100),
824                unique_items: true
825            }
826        ));
827    }
828
829    #[test]
830    fn test_schema_dto_to_domain_array_without_items() {
831        let dto = SchemaDefinitionDto::Array {
832            items: None,
833            min_items: None,
834            max_items: None,
835            unique_items: false,
836        };
837
838        let schema: Schema = dto.into();
839
840        assert!(matches!(
841            schema,
842            Schema::Array {
843                items: None,
844                min_items: None,
845                max_items: None,
846                unique_items: false
847            }
848        ));
849    }
850
851    #[test]
852    fn test_schema_dto_to_domain_object() {
853        let mut properties = HashMap::new();
854        properties.insert(
855            "id".to_string(),
856            SchemaDefinitionDto::Integer {
857                minimum: Some(1),
858                maximum: None,
859            },
860        );
861
862        let dto = SchemaDefinitionDto::Object {
863            properties,
864            required: vec!["id".to_string()],
865            additional_properties: false,
866        };
867
868        let schema: Schema = dto.into();
869
870        assert!(matches!(schema, Schema::Object {
871            properties,
872            required,
873            additional_properties: false
874        } if properties.len() == 1 && required.len() == 1));
875    }
876
877    #[test]
878    fn test_schema_dto_to_domain_oneof() {
879        let dto = SchemaDefinitionDto::OneOf {
880            schemas: vec![
881                SchemaDefinitionDto::String {
882                    min_length: None,
883                    max_length: None,
884                    pattern: None,
885                    enum_values: None,
886                },
887                SchemaDefinitionDto::Integer {
888                    minimum: None,
889                    maximum: None,
890                },
891            ],
892        };
893
894        let schema: Schema = dto.into();
895
896        assert!(matches!(schema, Schema::OneOf { schemas } if schemas.len() == 2));
897    }
898
899    #[test]
900    fn test_schema_dto_to_domain_allof() {
901        let dto = SchemaDefinitionDto::AllOf {
902            schemas: vec![
903                SchemaDefinitionDto::String {
904                    min_length: Some(1),
905                    max_length: None,
906                    pattern: None,
907                    enum_values: None,
908                },
909                SchemaDefinitionDto::String {
910                    min_length: None,
911                    max_length: Some(100),
912                    pattern: None,
913                    enum_values: None,
914                },
915            ],
916        };
917
918        let schema: Schema = dto.into();
919
920        assert!(matches!(schema, Schema::AllOf { schemas } if schemas.len() == 2));
921    }
922
923    #[test]
924    fn test_schema_dto_to_domain_any() {
925        let dto = SchemaDefinitionDto::Any;
926        let schema: Schema = dto.into();
927
928        assert!(matches!(schema, Schema::Any));
929    }
930
931    // ===========================================
932    // Domain Schema to DTO Conversion Tests
933    // ===========================================
934
935    #[test]
936    fn test_domain_schema_to_dto_string() {
937        let schema = Schema::String {
938            min_length: Some(10),
939            max_length: Some(100),
940            pattern: Some("[0-9]+".into()),
941            allowed_values: Some(smallvec::smallvec!["123".into(), "456".into()]),
942        };
943
944        let dto: SchemaDefinitionDto = (&schema).into();
945
946        assert!(matches!(
947            dto,
948            SchemaDefinitionDto::String {
949                min_length: Some(10),
950                max_length: Some(100),
951                pattern: Some(_),
952                enum_values: Some(_)
953            }
954        ));
955    }
956
957    #[test]
958    fn test_domain_schema_to_dto_integer() {
959        let schema = Schema::Integer {
960            minimum: Some(0),
961            maximum: Some(1000),
962        };
963
964        let dto: SchemaDefinitionDto = (&schema).into();
965
966        assert!(matches!(
967            dto,
968            SchemaDefinitionDto::Integer {
969                minimum: Some(0),
970                maximum: Some(1000)
971            }
972        ));
973    }
974
975    #[test]
976    fn test_domain_schema_to_dto_number() {
977        let schema = Schema::Number {
978            minimum: Some(0.0),
979            maximum: Some(99.99),
980        };
981
982        let dto: SchemaDefinitionDto = (&schema).into();
983
984        assert!(matches!(dto, SchemaDefinitionDto::Number { .. }));
985    }
986
987    #[test]
988    fn test_domain_schema_to_dto_boolean() {
989        let schema = Schema::Boolean;
990        let dto: SchemaDefinitionDto = (&schema).into();
991
992        assert!(matches!(dto, SchemaDefinitionDto::Boolean));
993    }
994
995    #[test]
996    fn test_domain_schema_to_dto_null() {
997        let schema = Schema::Null;
998        let dto: SchemaDefinitionDto = (&schema).into();
999
1000        assert!(matches!(dto, SchemaDefinitionDto::Null));
1001    }
1002
1003    #[test]
1004    fn test_domain_schema_to_dto_array() {
1005        let schema = Schema::Array {
1006            items: Some(Box::new(Schema::String {
1007                min_length: None,
1008                max_length: None,
1009                pattern: None,
1010                allowed_values: None,
1011            })),
1012            min_items: Some(1),
1013            max_items: Some(50),
1014            unique_items: true,
1015        };
1016
1017        let dto: SchemaDefinitionDto = (&schema).into();
1018
1019        assert!(matches!(
1020            dto,
1021            SchemaDefinitionDto::Array {
1022                items: Some(_),
1023                min_items: Some(1),
1024                max_items: Some(50),
1025                unique_items: true
1026            }
1027        ));
1028    }
1029
1030    #[test]
1031    fn test_domain_schema_to_dto_object() {
1032        let mut properties = HashMap::new();
1033        properties.insert(
1034            "email".to_string(),
1035            Schema::String {
1036                min_length: Some(5),
1037                max_length: Some(200),
1038                pattern: None,
1039                allowed_values: None,
1040            },
1041        );
1042
1043        let schema = Schema::Object {
1044            properties,
1045            required: vec!["email".to_string()],
1046            additional_properties: true,
1047        };
1048
1049        let dto: SchemaDefinitionDto = (&schema).into();
1050
1051        assert!(matches!(dto, SchemaDefinitionDto::Object {
1052            properties,
1053            required,
1054            additional_properties: true
1055        } if properties.len() == 1 && required.len() == 1));
1056    }
1057
1058    #[test]
1059    fn test_domain_schema_to_dto_oneof() {
1060        let schema = Schema::OneOf {
1061            schemas: smallvec::smallvec![
1062                Box::new(Schema::String {
1063                    min_length: None,
1064                    max_length: None,
1065                    pattern: None,
1066                    allowed_values: None,
1067                }),
1068                Box::new(Schema::Integer {
1069                    minimum: None,
1070                    maximum: None,
1071                }),
1072            ],
1073        };
1074
1075        let dto: SchemaDefinitionDto = (&schema).into();
1076
1077        assert!(matches!(dto, SchemaDefinitionDto::OneOf { schemas } if schemas.len() == 2));
1078    }
1079
1080    #[test]
1081    fn test_domain_schema_to_dto_allof() {
1082        let schema = Schema::AllOf {
1083            schemas: smallvec::smallvec![
1084                Box::new(Schema::String {
1085                    min_length: Some(1),
1086                    max_length: None,
1087                    pattern: None,
1088                    allowed_values: None,
1089                }),
1090                Box::new(Schema::String {
1091                    min_length: None,
1092                    max_length: Some(50),
1093                    pattern: None,
1094                    allowed_values: None,
1095                }),
1096            ],
1097        };
1098
1099        let dto: SchemaDefinitionDto = (&schema).into();
1100
1101        assert!(matches!(dto, SchemaDefinitionDto::AllOf { schemas } if schemas.len() == 2));
1102    }
1103
1104    #[test]
1105    fn test_domain_schema_to_dto_any() {
1106        let schema = Schema::Any;
1107        let dto: SchemaDefinitionDto = (&schema).into();
1108
1109        assert!(matches!(dto, SchemaDefinitionDto::Any));
1110    }
1111
1112    // ===========================================
1113    // SchemaRegistrationDto Tests
1114    // ===========================================
1115
1116    #[test]
1117    fn test_schema_registration_dto_serialization() {
1118        let dto = SchemaRegistrationDto {
1119            id: "user-schema".to_string(),
1120            schema: SchemaDefinitionDto::Object {
1121                properties: HashMap::new(),
1122                required: vec![],
1123                additional_properties: true,
1124            },
1125            metadata: Some(SchemaMetadataDto {
1126                version: "1.0".to_string(),
1127                description: Some("User schema".to_string()),
1128                author: Some("John Doe".to_string()),
1129                created_at: Some(1234567890),
1130            }),
1131        };
1132
1133        let json = serde_json::to_string(&dto).unwrap();
1134        let deserialized: SchemaRegistrationDto = serde_json::from_str(&json).unwrap();
1135
1136        assert_eq!(deserialized.id, "user-schema");
1137        assert!(matches!(
1138            deserialized.schema,
1139            SchemaDefinitionDto::Object { .. }
1140        ));
1141        assert!(deserialized.metadata.is_some());
1142    }
1143
1144    #[test]
1145    fn test_schema_registration_dto_without_metadata() {
1146        let dto = SchemaRegistrationDto {
1147            id: "simple-schema".to_string(),
1148            schema: SchemaDefinitionDto::String {
1149                min_length: None,
1150                max_length: None,
1151                pattern: None,
1152                enum_values: None,
1153            },
1154            metadata: None,
1155        };
1156
1157        let json = serde_json::to_string(&dto).unwrap();
1158        let deserialized: SchemaRegistrationDto = serde_json::from_str(&json).unwrap();
1159
1160        assert_eq!(deserialized.id, "simple-schema");
1161        assert!(deserialized.metadata.is_none());
1162    }
1163
1164    // ===========================================
1165    // SchemaMetadataDto Tests
1166    // ===========================================
1167
1168    #[test]
1169    fn test_schema_metadata_dto_full() {
1170        let dto = SchemaMetadataDto {
1171            version: "2.5".to_string(),
1172            description: Some("Complete metadata".to_string()),
1173            author: Some("Jane Smith".to_string()),
1174            created_at: Some(9876543210),
1175        };
1176
1177        let json = serde_json::to_string(&dto).unwrap();
1178        let deserialized: SchemaMetadataDto = serde_json::from_str(&json).unwrap();
1179
1180        assert_eq!(deserialized.version, "2.5");
1181        assert_eq!(
1182            deserialized.description,
1183            Some("Complete metadata".to_string())
1184        );
1185        assert_eq!(deserialized.author, Some("Jane Smith".to_string()));
1186        assert_eq!(deserialized.created_at, Some(9876543210));
1187    }
1188
1189    #[test]
1190    fn test_schema_metadata_dto_minimal() {
1191        let dto = SchemaMetadataDto {
1192            version: "1.0".to_string(),
1193            description: None,
1194            author: None,
1195            created_at: None,
1196        };
1197
1198        let json = serde_json::to_string(&dto).unwrap();
1199        let deserialized: SchemaMetadataDto = serde_json::from_str(&json).unwrap();
1200
1201        assert_eq!(deserialized.version, "1.0");
1202        assert!(deserialized.description.is_none());
1203        assert!(deserialized.author.is_none());
1204        assert!(deserialized.created_at.is_none());
1205    }
1206
1207    // ===========================================
1208    // ValidationRequestDto and ValidationResultDto Tests
1209    // ===========================================
1210
1211    #[test]
1212    fn test_validation_request_dto_serialization() {
1213        let dto = ValidationRequestDto {
1214            schema_id: "user-schema".to_string(),
1215            data: r#"{"name": "John", "age": 30}"#.to_string(),
1216        };
1217
1218        let json = serde_json::to_string(&dto).unwrap();
1219        let deserialized: ValidationRequestDto = serde_json::from_str(&json).unwrap();
1220
1221        assert_eq!(deserialized.schema_id, "user-schema");
1222        assert_eq!(deserialized.data, r#"{"name": "John", "age": 30}"#);
1223    }
1224
1225    #[test]
1226    fn test_validation_result_dto_valid() {
1227        let dto = ValidationResultDto {
1228            valid: true,
1229            errors: vec![],
1230        };
1231
1232        let json = serde_json::to_string(&dto).unwrap();
1233        // When errors is empty, it's not serialized due to skip_serializing_if
1234        // So we add it back for deserialization
1235        let json_with_errors = if json.contains("errors") {
1236            json
1237        } else {
1238            json.replace("}", r#","errors":[]}"#)
1239        };
1240        let deserialized: ValidationResultDto = serde_json::from_str(&json_with_errors).unwrap();
1241
1242        assert!(deserialized.valid);
1243        assert!(deserialized.errors.is_empty());
1244    }
1245
1246    #[test]
1247    fn test_validation_result_dto_with_errors() {
1248        let dto = ValidationResultDto {
1249            valid: false,
1250            errors: vec![
1251                ValidationErrorDto {
1252                    path: "$.name".to_string(),
1253                    message: "Too short".to_string(),
1254                    error_type: "string_length".to_string(),
1255                },
1256                ValidationErrorDto {
1257                    path: "$.age".to_string(),
1258                    message: "Out of range".to_string(),
1259                    error_type: "out_of_range".to_string(),
1260                },
1261            ],
1262        };
1263
1264        let json = serde_json::to_string(&dto).unwrap();
1265        let deserialized: ValidationResultDto = serde_json::from_str(&json).unwrap();
1266
1267        assert!(!deserialized.valid);
1268        assert_eq!(deserialized.errors.len(), 2);
1269    }
1270
1271    // ===========================================
1272    // ValidationErrorDto Conversion Tests
1273    // ===========================================
1274
1275    #[test]
1276    fn test_validation_error_type_mismatch_conversion() {
1277        let domain_error = SchemaValidationError::TypeMismatch {
1278            path: "$.field".to_string(),
1279            expected: "string".to_string(),
1280            actual: "number".to_string(),
1281        };
1282
1283        let dto: ValidationErrorDto = (&domain_error).into();
1284
1285        assert_eq!(dto.path, "$.field");
1286        assert_eq!(dto.error_type, "type_mismatch");
1287        assert!(dto.message.contains("string"));
1288        assert!(dto.message.contains("number"));
1289    }
1290
1291    #[test]
1292    fn test_validation_error_missing_required_conversion() {
1293        let domain_error = SchemaValidationError::MissingRequired {
1294            path: "$.".to_string(),
1295            field: "email".to_string(),
1296        };
1297
1298        let dto: ValidationErrorDto = (&domain_error).into();
1299
1300        assert_eq!(dto.path, "$.");
1301        assert_eq!(dto.error_type, "missing_required");
1302        assert!(dto.message.contains("email"));
1303    }
1304
1305    #[test]
1306    fn test_validation_error_out_of_range_conversion() {
1307        let domain_error = SchemaValidationError::OutOfRange {
1308            path: "$.age".to_string(),
1309            value: "200".to_string(),
1310            min: "0".to_string(),
1311            max: "150".to_string(),
1312        };
1313
1314        let dto: ValidationErrorDto = (&domain_error).into();
1315
1316        assert_eq!(dto.path, "$.age");
1317        assert_eq!(dto.error_type, "out_of_range");
1318        assert!(dto.message.contains("200"));
1319    }
1320
1321    #[test]
1322    fn test_validation_error_string_length_conversion() {
1323        let domain_error = SchemaValidationError::StringLengthConstraint {
1324            path: "$.name".to_string(),
1325            actual: 150,
1326            min: 1,
1327            max: 100,
1328        };
1329
1330        let dto: ValidationErrorDto = (&domain_error).into();
1331
1332        assert_eq!(dto.path, "$.name");
1333        assert_eq!(dto.error_type, "string_length");
1334        assert!(dto.message.contains("150"));
1335    }
1336
1337    #[test]
1338    fn test_validation_error_pattern_mismatch_conversion() {
1339        let domain_error = SchemaValidationError::PatternMismatch {
1340            path: "$.email".to_string(),
1341            value: "invalid".to_string(),
1342            pattern: "[a-z]+@[a-z]+\\.[a-z]+".to_string(),
1343        };
1344
1345        let dto: ValidationErrorDto = (&domain_error).into();
1346
1347        assert_eq!(dto.path, "$.email");
1348        assert_eq!(dto.error_type, "pattern_mismatch");
1349        assert!(dto.message.contains("invalid"));
1350    }
1351
1352    #[test]
1353    fn test_validation_error_array_size_conversion() {
1354        let domain_error = SchemaValidationError::ArraySizeConstraint {
1355            path: "$.items".to_string(),
1356            actual: 20,
1357            min: 1,
1358            max: 10,
1359        };
1360
1361        let dto: ValidationErrorDto = (&domain_error).into();
1362
1363        assert_eq!(dto.path, "$.items");
1364        assert_eq!(dto.error_type, "array_size");
1365        assert!(dto.message.contains("20"));
1366    }
1367
1368    #[test]
1369    fn test_validation_error_duplicate_items_conversion() {
1370        let domain_error = SchemaValidationError::DuplicateItems {
1371            path: "$.values".to_string(),
1372        };
1373
1374        let dto: ValidationErrorDto = (&domain_error).into();
1375
1376        assert_eq!(dto.path, "$.values");
1377        assert_eq!(dto.error_type, "duplicate_items");
1378        assert!(dto.message.contains("duplicate"));
1379    }
1380
1381    #[test]
1382    fn test_validation_error_invalid_enum_conversion() {
1383        let domain_error = SchemaValidationError::InvalidEnumValue {
1384            path: "$.status".to_string(),
1385            value: "pending".to_string(),
1386        };
1387
1388        let dto: ValidationErrorDto = (&domain_error).into();
1389
1390        assert_eq!(dto.path, "$.status");
1391        assert_eq!(dto.error_type, "invalid_enum");
1392        assert!(dto.message.contains("pending"));
1393    }
1394
1395    #[test]
1396    fn test_validation_error_additional_property_conversion() {
1397        let domain_error = SchemaValidationError::AdditionalPropertyNotAllowed {
1398            path: "$.".to_string(),
1399            property: "extra_field".to_string(),
1400        };
1401
1402        let dto: ValidationErrorDto = (&domain_error).into();
1403
1404        assert_eq!(dto.path, "$.");
1405        assert_eq!(dto.error_type, "additional_property");
1406        assert!(dto.message.contains("extra_field"));
1407    }
1408
1409    #[test]
1410    fn test_validation_error_no_matching_oneof_conversion() {
1411        let domain_error = SchemaValidationError::NoMatchingOneOf {
1412            path: "$.value".to_string(),
1413        };
1414
1415        let dto: ValidationErrorDto = (&domain_error).into();
1416
1417        assert_eq!(dto.path, "$.value");
1418        assert_eq!(dto.error_type, "no_matching_one_of");
1419    }
1420
1421    #[test]
1422    fn test_validation_error_allof_failure_conversion() {
1423        let domain_error = SchemaValidationError::AllOfFailure {
1424            path: "$.item".to_string(),
1425            failures: "schema1, schema2".to_string(),
1426        };
1427
1428        let dto: ValidationErrorDto = (&domain_error).into();
1429
1430        assert_eq!(dto.path, "$.item");
1431        assert_eq!(dto.error_type, "all_of_failure");
1432        assert!(dto.message.contains("schema1"));
1433    }
1434
1435    // ===========================================
1436    // Complex Nested Schema Tests
1437    // ===========================================
1438
1439    #[test]
1440    fn test_nested_object_with_array_conversion() {
1441        let mut inner_properties = HashMap::new();
1442        inner_properties.insert(
1443            "id".to_string(),
1444            SchemaDefinitionDto::Integer {
1445                minimum: Some(1),
1446                maximum: None,
1447            },
1448        );
1449
1450        let dto = SchemaDefinitionDto::Object {
1451            properties: {
1452                let mut props = HashMap::new();
1453                props.insert(
1454                    "items".to_string(),
1455                    SchemaDefinitionDto::Array {
1456                        items: Some(Box::new(SchemaDefinitionDto::Object {
1457                            properties: inner_properties,
1458                            required: vec!["id".to_string()],
1459                            additional_properties: false,
1460                        })),
1461                        min_items: Some(1),
1462                        max_items: None,
1463                        unique_items: false,
1464                    },
1465                );
1466                props
1467            },
1468            required: vec!["items".to_string()],
1469            additional_properties: true,
1470        };
1471
1472        let schema: Schema = dto.into();
1473        assert!(matches!(schema, Schema::Object { .. }));
1474    }
1475
1476    #[test]
1477    fn test_nested_object_roundtrip() {
1478        let mut inner_props = HashMap::new();
1479        inner_props.insert(
1480            "name".to_string(),
1481            Schema::String {
1482                min_length: Some(1),
1483                max_length: Some(100),
1484                pattern: None,
1485                allowed_values: None,
1486            },
1487        );
1488
1489        let original_schema = Schema::Object {
1490            properties: {
1491                let mut props = HashMap::new();
1492                props.insert(
1493                    "user".to_string(),
1494                    Schema::Object {
1495                        properties: inner_props,
1496                        required: vec!["name".to_string()],
1497                        additional_properties: false,
1498                    },
1499                );
1500                props
1501            },
1502            required: vec!["user".to_string()],
1503            additional_properties: true,
1504        };
1505
1506        let dto: SchemaDefinitionDto = (&original_schema).into();
1507        let schema: Schema = dto.into();
1508
1509        assert!(matches!(schema, Schema::Object { .. }));
1510    }
1511
1512    #[test]
1513    fn test_deeply_nested_array() {
1514        let innermost_dto = SchemaDefinitionDto::String {
1515            min_length: None,
1516            max_length: None,
1517            pattern: None,
1518            enum_values: None,
1519        };
1520
1521        let level1 = SchemaDefinitionDto::Array {
1522            items: Some(Box::new(innermost_dto)),
1523            min_items: None,
1524            max_items: None,
1525            unique_items: false,
1526        };
1527
1528        let level2 = SchemaDefinitionDto::Array {
1529            items: Some(Box::new(level1)),
1530            min_items: None,
1531            max_items: None,
1532            unique_items: false,
1533        };
1534
1535        let schema: Schema = level2.into();
1536
1537        assert!(matches!(schema, Schema::Array {
1538            items: Some(boxed),
1539            ..
1540        } if matches!(*boxed, Schema::Array { .. })));
1541    }
1542
1543    // ===========================================
1544    // Deserialization Depth Guard Tests
1545    // ===========================================
1546
1547    /// Builds `levels` nested `SchemaDefinitionDto::Array` wrappers around a
1548    /// `Boolean` leaf, iteratively (not recursively), so construction itself
1549    /// never recurses on the Rust call stack regardless of `levels`.
1550    fn nested_array_dto(levels: usize) -> SchemaDefinitionDto {
1551        let mut dto = SchemaDefinitionDto::Boolean;
1552        for _ in 0..levels {
1553            dto = SchemaDefinitionDto::Array {
1554                items: Some(Box::new(dto)),
1555                min_items: None,
1556                max_items: None,
1557                unique_items: false,
1558            };
1559        }
1560        dto
1561    }
1562
1563    // `to_vec_named` (map-based struct encoding), not the default `to_vec`
1564    // (positional/array-based): several fields on `SchemaDefinitionDto` use
1565    // `#[serde(skip_serializing_if = "Option::is_none")]` and are not the
1566    // struct's last field (e.g. `Array::items`), so positional encoding
1567    // shortens the emitted array and misaligns every field after the
1568    // skipped one on decode — unrelated to the depth guard under test here.
1569
1570    #[test]
1571    fn test_schema_dto_deserialize_at_max_depth_succeeds() {
1572        let dto = nested_array_dto(pjson_rs_domain::MAX_DESERIALIZE_DEPTH);
1573        let bytes = rmp_serde::to_vec_named(&dto).unwrap();
1574        let result: Result<SchemaDefinitionDto, _> = rmp_serde::from_slice(&bytes);
1575        assert!(result.is_ok(), "{:?}", result.err());
1576    }
1577
1578    #[test]
1579    fn test_schema_dto_deserialize_beyond_max_depth_rejected() {
1580        let dto = nested_array_dto(pjson_rs_domain::MAX_DESERIALIZE_DEPTH + 1);
1581        let bytes = rmp_serde::to_vec_named(&dto).unwrap();
1582        let err = rmp_serde::from_slice::<SchemaDefinitionDto>(&bytes)
1583            .unwrap_err()
1584            .to_string();
1585        // Assert on our message, not just is_err(), so the test cannot pass
1586        // because rmp-serde rejected the input for an unrelated reason.
1587        assert!(err.contains("SchemaDefinitionDto nesting depth"), "{err}");
1588    }
1589
1590    #[test]
1591    fn test_schema_dto_deserialize_small_nesting_roundtrips_via_json() {
1592        let dto = nested_array_dto(3);
1593        let json = serde_json::to_string(&dto).unwrap();
1594        let back: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
1595        let original_schema: Schema = dto.into();
1596        let roundtrip_schema: Schema = back.into();
1597        assert_eq!(original_schema, roundtrip_schema);
1598    }
1599
1600    /// `Array::items` must stay optional: `#[serde(deserialize_with = ...)]`
1601    /// alone suppresses serde derive's implicit "absent `Option<T>` field ->
1602    /// `None`" behavior, which would make `items` newly required and break
1603    /// any persisted `SchemaDefinitionDto::Array` document that omits it.
1604    /// The `#[serde(default, deserialize_with = ...)]` combination on the
1605    /// field restores the pre-guard optional behavior.
1606    #[test]
1607    fn test_schema_dto_array_items_still_optional_when_absent() {
1608        let json = r#"{"Array":{"min_items":null,"max_items":null,"unique_items":false}}"#;
1609        let dto: SchemaDefinitionDto = serde_json::from_str(json).unwrap();
1610        assert!(matches!(
1611            dto,
1612            SchemaDefinitionDto::Array { items: None, .. }
1613        ));
1614    }
1615
1616    /// `Object::properties` and `OneOf`/`AllOf::schemas` were already
1617    /// required fields before the depth guard (non-`Option` types with no
1618    /// `#[serde(default)]`), so adding `deserialize_with` to them — unlike
1619    /// `Array::items` above — doesn't change their required-ness.
1620    #[test]
1621    fn test_schema_dto_properties_and_schemas_still_required() {
1622        let json = r#"{"Object":{"required":[],"additional_properties":true}}"#;
1623        let result: Result<SchemaDefinitionDto, _> = serde_json::from_str(json);
1624        assert!(result.is_err(), "properties should still be required");
1625
1626        let json = r#"{"OneOf":{}}"#;
1627        let result: Result<SchemaDefinitionDto, _> = serde_json::from_str(json);
1628        assert!(result.is_err(), "schemas should still be required");
1629    }
1630}