Skip to main content

pjson_rs_domain/value_objects/
schema.rs

1//! Schema value object for JSON validation
2//!
3//! Represents a JSON schema definition following a subset of JSON Schema specification.
4//! This is a domain value object with no identity, defined purely by its attributes.
5
6use serde::{Deserialize, Deserializer, Serialize};
7use smallvec::SmallVec;
8use std::cell::Cell;
9use std::collections::HashMap;
10
11use crate::DomainError;
12#[cfg(test)]
13use crate::value_objects::MAX_DESERIALIZE_DEPTH;
14use crate::value_objects::enter_deserialize_depth;
15
16thread_local! {
17    /// Per-thread nesting depth reached while deserializing [`Schema`].
18    ///
19    /// [`Schema`] is a recursive enum (`Array::items: Box<Schema>`,
20    /// `Object::properties: HashMap<String, Schema>`, and
21    /// `OneOf`/`AllOf::schemas: SmallVec<[Box<Schema>; 4]>`), so nesting depth
22    /// in the input maps directly onto recursive `Schema::deserialize` calls
23    /// and their stack frames. Without a bound, a deeply nested schema
24    /// document exhausts the stack (CWE-674) for any self-describing
25    /// deserializer that reaches `Schema::deserialize` recursively —
26    /// `serde_json`'s own ~128-level limit happens to guard the in-tree HTTP
27    /// path today, but `Schema::deserialize` is public API with no such
28    /// guarantee against other formats (MessagePack, CBOR, etc.), the same
29    /// gap [`crate::value_objects::JsonData`]'s depth guard closes.
30    ///
31    /// Because `Schema`'s externally-tagged wire encoding wraps each nesting
32    /// level in 2-3 JSON container tokens (varies by variant — an `Object`
33    /// level costs 3, an `Array` level costs 2), `serde_json`'s own
34    /// ~128-level structural limit is reached at a *lower* `Schema` nesting
35    /// depth than [`crate::value_objects::MAX_DESERIALIZE_DEPTH`] (as low as ~43 for `Object`-heavy
36    /// nesting) and fires first with its own `"recursion limit exceeded"`
37    /// message — this guard's own `"Schema nesting depth exceeds maximum of
38    /// N"` message is reachable in practice only via formats with no
39    /// competing structural recursion limit of their own (MessagePack, CBOR,
40    /// etc.), which is exactly the gap this guard closes.
41    ///
42    /// `JsonData`'s guard threads depth through a
43    /// [`serde::de::DeserializeSeed`]-based hand-written `Visitor`, which
44    /// works because `JsonData::deserialize` is itself hand-written.
45    /// `Schema::deserialize` is `#[derive(Deserialize)]`d for a ten-variant
46    /// enum, and `#[derive(Deserialize)]`-generated field deserialization
47    /// only ever hands a `deserialize_with` function a `Deserializer` — there
48    /// is no way to thread a `DeserializeSeed` (or any other caller state)
49    /// through derive-generated code without hand-writing a full custom
50    /// `Visitor` for all ten variants. A thread-local counter, incremented on
51    /// entry to each of `Schema`'s three recursive field positions via
52    /// [`enter_deserialize_depth`] and decremented by its returned
53    /// [`crate::value_objects::DepthGuard`] on drop (so the count restores
54    /// correctly even when deserialization returns an error or the call
55    /// unwinds), is the practical alternative. Every top-level
56    /// `Schema::deserialize` call both starts and ends at depth 0 on its
57    /// thread.
58    static SCHEMA_DESERIALIZE_DEPTH: Cell<usize> = const { Cell::new(0) };
59}
60
61/// Bounded-depth `deserialize_with` for `Schema::Array`'s `items` field.
62fn deserialize_boxed_schema_option<'de, D>(deserializer: D) -> Result<Option<Box<Schema>>, D::Error>
63where
64    D: Deserializer<'de>,
65{
66    let _guard = enter_deserialize_depth::<D::Error>(&SCHEMA_DESERIALIZE_DEPTH, "Schema")?;
67    Option::<Box<Schema>>::deserialize(deserializer)
68}
69
70/// Bounded-depth `deserialize_with` for `Schema::Object`'s `properties` field.
71fn deserialize_schema_properties<'de, D>(
72    deserializer: D,
73) -> Result<HashMap<String, Schema>, D::Error>
74where
75    D: Deserializer<'de>,
76{
77    let _guard = enter_deserialize_depth::<D::Error>(&SCHEMA_DESERIALIZE_DEPTH, "Schema")?;
78    HashMap::<String, Schema>::deserialize(deserializer)
79}
80
81/// Bounded-depth `deserialize_with` for `Schema::OneOf`/`AllOf`'s `schemas` field.
82fn deserialize_schema_list<'de, D>(deserializer: D) -> Result<SmallVec<[Box<Schema>; 4]>, D::Error>
83where
84    D: Deserializer<'de>,
85{
86    let _guard = enter_deserialize_depth::<D::Error>(&SCHEMA_DESERIALIZE_DEPTH, "Schema")?;
87    SmallVec::<[Box<Schema>; 4]>::deserialize(deserializer)
88}
89
90/// Schema identifier for tracking and referencing schemas
91#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
92pub struct SchemaId(String);
93
94impl SchemaId {
95    /// Create a new schema identifier
96    ///
97    /// # Arguments
98    /// * `id` - Unique schema identifier string
99    ///
100    /// # Returns
101    /// New schema ID instance
102    ///
103    /// # Examples
104    /// ```
105    /// # use pjson_rs_domain::value_objects::SchemaId;
106    /// let schema_id = SchemaId::new("user-profile-v1");
107    /// ```
108    pub fn new(id: impl Into<String>) -> Self {
109        Self(id.into())
110    }
111
112    /// Get schema ID as string slice
113    pub fn as_str(&self) -> &str {
114        &self.0
115    }
116}
117
118impl std::fmt::Display for SchemaId {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        write!(f, "{}", self.0)
121    }
122}
123
124/// JSON Schema representation for validation
125///
126/// Supports a practical subset of JSON Schema Draft 2020-12 focused on
127/// validation needs for streaming JSON data.
128///
129/// # Design Philosophy
130/// - Focused on validation, not full JSON Schema specification
131/// - Performance-oriented with pre-compiled validation rules
132/// - Zero-copy where possible using Arc for shared data
133/// - Type-safe with strongly-typed enum variants
134///
135/// # Examples
136/// ```
137/// # use pjson_rs_domain::value_objects::{Schema, SchemaType};
138/// let schema = Schema::Object {
139///     properties: vec![
140///         ("id".to_string(), Schema::Integer { minimum: Some(1), maximum: None }),
141///         ("name".to_string(), Schema::String {
142///             min_length: Some(1),
143///             max_length: Some(100),
144///             pattern: None,
145///             allowed_values: None,
146///         }),
147///     ].into_iter().collect(),
148///     required: vec!["id".to_string(), "name".to_string()],
149///     additional_properties: false,
150/// };
151/// ```
152#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
153#[non_exhaustive]
154pub enum Schema {
155    /// String type with optional constraints
156    String {
157        /// Minimum string length (inclusive)
158        min_length: Option<usize>,
159        /// Maximum string length (inclusive)
160        max_length: Option<usize>,
161        /// Pattern to match (regex)
162        pattern: Option<String>,
163        /// Enumeration of allowed values
164        allowed_values: Option<SmallVec<[String; 8]>>,
165    },
166
167    /// Integer type with optional range constraints
168    Integer {
169        /// Minimum value (inclusive)
170        minimum: Option<i64>,
171        /// Maximum value (inclusive)
172        maximum: Option<i64>,
173    },
174
175    /// Number type (float/double) with optional range constraints
176    Number {
177        /// Minimum value (inclusive)
178        minimum: Option<f64>,
179        /// Maximum value (inclusive)
180        maximum: Option<f64>,
181    },
182
183    /// Boolean type (no constraints)
184    Boolean,
185
186    /// Null type (no constraints)
187    Null,
188
189    /// Array type with element schema and size constraints
190    Array {
191        /// Schema for array elements (None = any type)
192        #[serde(default, deserialize_with = "deserialize_boxed_schema_option")]
193        items: Option<Box<Schema>>,
194        /// Minimum array length (inclusive)
195        min_items: Option<usize>,
196        /// Maximum array length (inclusive)
197        max_items: Option<usize>,
198        /// Whether all items must be unique
199        unique_items: bool,
200    },
201
202    /// Object type with property schemas
203    Object {
204        /// Property name to schema mapping
205        #[serde(deserialize_with = "deserialize_schema_properties")]
206        properties: HashMap<String, Schema>,
207        /// List of required property names
208        required: Vec<String>,
209        /// Whether additional properties are allowed
210        additional_properties: bool,
211    },
212
213    /// Union type (one of multiple schemas)
214    OneOf {
215        /// List of possible schemas
216        #[serde(deserialize_with = "deserialize_schema_list")]
217        schemas: SmallVec<[Box<Schema>; 4]>,
218    },
219
220    /// Intersection type (all of multiple schemas)
221    AllOf {
222        /// List of schemas that must all match
223        #[serde(deserialize_with = "deserialize_schema_list")]
224        schemas: SmallVec<[Box<Schema>; 4]>,
225    },
226
227    /// Any type (no validation)
228    Any,
229}
230
231/// Schema validation result
232pub type SchemaValidationResult<T> = Result<T, SchemaValidationError>;
233
234/// Schema validation error with detailed context
235///
236/// Provides rich error information including the JSON path where validation failed,
237/// expected vs actual values, and human-readable error messages.
238///
239/// # Design
240/// - Includes full path context for nested validation failures
241/// - Provides actionable error messages for debugging
242/// - Zero-allocation for common error cases using `String`
243#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, thiserror::Error)]
244#[non_exhaustive]
245pub enum SchemaValidationError {
246    /// Type mismatch error
247    #[error("Type mismatch at '{path}': expected {expected}, got {actual}")]
248    TypeMismatch {
249        /// JSON path where error occurred
250        path: String,
251        /// Expected type
252        expected: String,
253        /// Actual type
254        actual: String,
255    },
256
257    /// Missing required field
258    #[error("Missing required field at '{path}': {field}")]
259    MissingRequired {
260        /// JSON path to parent object
261        path: String,
262        /// Name of missing field
263        field: String,
264    },
265
266    /// Value out of range
267    #[error("Value out of range at '{path}': {value} not in [{min}, {max}]")]
268    OutOfRange {
269        /// JSON path where error occurred
270        path: String,
271        /// Actual value
272        value: String,
273        /// Minimum allowed value
274        min: String,
275        /// Maximum allowed value
276        max: String,
277    },
278
279    /// String length constraint violation
280    #[error("String length constraint at '{path}': length {actual} not in [{min}, {max}]")]
281    StringLengthConstraint {
282        /// JSON path where error occurred
283        path: String,
284        /// Actual string length
285        actual: usize,
286        /// Minimum allowed length
287        min: usize,
288        /// Maximum allowed length
289        max: usize,
290    },
291
292    /// Pattern mismatch
293    #[error("Pattern mismatch at '{path}': value '{value}' does not match pattern '{pattern}'")]
294    PatternMismatch {
295        /// JSON path where error occurred
296        path: String,
297        /// Actual value
298        value: String,
299        /// Expected pattern
300        pattern: String,
301    },
302
303    /// Invalid regex pattern in schema
304    #[error("Invalid pattern at '{path}': pattern '{pattern}' is not valid regex: {reason}")]
305    InvalidPattern {
306        /// JSON path where error occurred
307        path: String,
308        /// The invalid pattern string
309        pattern: String,
310        /// Regex compilation error message
311        reason: String,
312    },
313
314    /// Regex pattern exceeded the maximum accepted length
315    ///
316    /// Returned before the pattern is compiled, so oversized patterns never
317    /// reach [`Self::InvalidPattern`] or [`Self::PatternMismatch`].
318    #[error("Pattern too long at '{path}': length {length} exceeds maximum {max}")]
319    PatternTooLong {
320        /// JSON path where error occurred
321        path: String,
322        /// Actual pattern length in bytes
323        length: usize,
324        /// Maximum accepted pattern length in bytes
325        max: usize,
326    },
327
328    /// Array size constraint violation
329    #[error("Array size constraint at '{path}': size {actual} not in [{min}, {max}]")]
330    ArraySizeConstraint {
331        /// JSON path where error occurred
332        path: String,
333        /// Actual array size
334        actual: usize,
335        /// Minimum allowed size
336        min: usize,
337        /// Maximum allowed size
338        max: usize,
339    },
340
341    /// Unique items constraint violation
342    #[error("Unique items constraint at '{path}': duplicate items found")]
343    DuplicateItems {
344        /// JSON path where error occurred
345        path: String,
346    },
347
348    /// Invalid enum value
349    #[error("Invalid enum value at '{path}': '{value}' not in allowed values")]
350    InvalidEnumValue {
351        /// JSON path where error occurred
352        path: String,
353        /// Actual value
354        value: String,
355    },
356
357    /// Additional properties not allowed
358    #[error("Additional property not allowed at '{path}': '{property}'")]
359    AdditionalPropertyNotAllowed {
360        /// JSON path where error occurred
361        path: String,
362        /// Property name
363        property: String,
364    },
365
366    /// No matching schema in OneOf
367    #[error("No matching schema in OneOf at '{path}'")]
368    NoMatchingOneOf {
369        /// JSON path where error occurred
370        path: String,
371    },
372
373    /// Not all schemas match in AllOf
374    #[error("Not all schemas match in AllOf at '{path}': {failures}")]
375    AllOfFailure {
376        /// JSON path where error occurred
377        path: String,
378        /// List of failing schema indices
379        failures: String,
380    },
381}
382
383impl Schema {
384    /// Check if schema allows a specific type
385    ///
386    /// Used for quick type compatibility checks before full validation.
387    ///
388    /// # Arguments
389    /// * `schema_type` - The type to check compatibility for
390    ///
391    /// # Returns
392    /// `true` if the schema allows the type, `false` otherwise
393    pub fn allows_type(&self, schema_type: SchemaType) -> bool {
394        match (self, schema_type) {
395            (Self::String { .. }, SchemaType::String) => true,
396            (Self::Integer { .. }, SchemaType::Integer) => true,
397            (Self::Number { .. }, SchemaType::Number) => true,
398            (Self::Boolean, SchemaType::Boolean) => true,
399            (Self::Null, SchemaType::Null) => true,
400            (Self::Array { .. }, SchemaType::Array) => true,
401            (Self::Object { .. }, SchemaType::Object) => true,
402            (Self::Any, _) => true,
403            (Self::OneOf { schemas }, schema_type) => {
404                schemas.iter().any(|s| s.allows_type(schema_type))
405            }
406            (Self::AllOf { schemas }, schema_type) => {
407                schemas.iter().all(|s| s.allows_type(schema_type))
408            }
409            _ => false,
410        }
411    }
412
413    /// Get estimated validation cost for performance optimization
414    ///
415    /// Higher cost indicates more expensive validation operations.
416    /// Used by validation scheduler to optimize validation order.
417    ///
418    /// # Returns
419    /// Validation cost estimate (0-1000 range)
420    pub fn validation_cost(&self) -> usize {
421        match self {
422            Self::Null | Self::Boolean | Self::Any => 1,
423            Self::Integer { .. } | Self::Number { .. } => 5,
424            Self::String {
425                pattern: Some(_), ..
426            } => 50, // Regex is expensive
427            Self::String { .. } => 10,
428            Self::Array { items, .. } => {
429                let item_cost = items.as_ref().map_or(1, |s| s.validation_cost());
430                10 + item_cost
431            }
432            Self::Object { properties, .. } => {
433                let prop_cost: usize = properties.values().map(|s| s.validation_cost()).sum();
434                20 + prop_cost
435            }
436            Self::OneOf { schemas } => {
437                let max_cost = schemas
438                    .iter()
439                    .map(|s| s.validation_cost())
440                    .max()
441                    .unwrap_or(0);
442                30 + max_cost * schemas.len()
443            }
444            Self::AllOf { schemas } => {
445                let total_cost: usize = schemas.iter().map(|s| s.validation_cost()).sum();
446                20 + total_cost
447            }
448        }
449    }
450
451    /// Create a simple string schema with length constraints
452    pub fn string(min_length: Option<usize>, max_length: Option<usize>) -> Self {
453        Self::String {
454            min_length,
455            max_length,
456            pattern: None,
457            allowed_values: None,
458        }
459    }
460
461    /// Create a simple integer schema with range constraints
462    pub fn integer(minimum: Option<i64>, maximum: Option<i64>) -> Self {
463        Self::Integer { minimum, maximum }
464    }
465
466    /// Create a simple number schema with range constraints
467    pub fn number(minimum: Option<f64>, maximum: Option<f64>) -> Self {
468        Self::Number { minimum, maximum }
469    }
470
471    /// Create an array schema with item type
472    pub fn array(items: Option<Schema>) -> Self {
473        Self::Array {
474            items: items.map(Box::new),
475            min_items: None,
476            max_items: None,
477            unique_items: false,
478        }
479    }
480
481    /// Create an object schema with properties
482    pub fn object(properties: HashMap<String, Schema>, required: Vec<String>) -> Self {
483        Self::Object {
484            properties,
485            required,
486            additional_properties: true,
487        }
488    }
489}
490
491/// Simplified schema type for quick type checking
492#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
493#[non_exhaustive]
494pub enum SchemaType {
495    /// String type
496    String,
497    /// Integer type
498    Integer,
499    /// Floating-point number type
500    Number,
501    /// Boolean type
502    Boolean,
503    /// Null type
504    Null,
505    /// Array type
506    Array,
507    /// Object type
508    Object,
509}
510
511impl From<&Schema> for SchemaType {
512    fn from(schema: &Schema) -> Self {
513        match schema {
514            Schema::String { .. } => Self::String,
515            Schema::Integer { .. } => Self::Integer,
516            Schema::Number { .. } => Self::Number,
517            Schema::Boolean => Self::Boolean,
518            Schema::Null => Self::Null,
519            Schema::Array { .. } => Self::Array,
520            Schema::Object { .. } => Self::Object,
521            Schema::Any => Self::Object, // Default to most flexible
522            Schema::OneOf { .. } | Schema::AllOf { .. } => Self::Object,
523        }
524    }
525}
526
527impl From<DomainError> for SchemaValidationError {
528    fn from(error: DomainError) -> Self {
529        match error {
530            DomainError::ValidationError(msg) => Self::TypeMismatch {
531                path: "/".to_string(),
532                expected: "valid".to_string(),
533                actual: msg,
534            },
535            _ => Self::TypeMismatch {
536                path: "/".to_string(),
537                expected: "valid".to_string(),
538                actual: error.to_string(),
539            },
540        }
541    }
542}
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547
548    #[test]
549    fn test_schema_id_creation() {
550        let id = SchemaId::new("test-schema-v1");
551        assert_eq!(id.as_str(), "test-schema-v1");
552        assert_eq!(id.to_string(), "test-schema-v1");
553    }
554
555    #[test]
556    fn test_schema_allows_type() {
557        let string_schema = Schema::string(Some(1), Some(100));
558        assert!(string_schema.allows_type(SchemaType::String));
559        assert!(!string_schema.allows_type(SchemaType::Integer));
560
561        let any_schema = Schema::Any;
562        assert!(any_schema.allows_type(SchemaType::String));
563        assert!(any_schema.allows_type(SchemaType::Integer));
564    }
565
566    #[test]
567    fn test_validation_cost() {
568        let simple = Schema::Boolean;
569        assert_eq!(simple.validation_cost(), 1);
570
571        let complex = Schema::Object {
572            properties: [
573                ("id".to_string(), Schema::integer(None, None)),
574                ("name".to_string(), Schema::string(Some(1), Some(100))),
575            ]
576            .into_iter()
577            .collect(),
578            required: vec!["id".to_string()],
579            additional_properties: false,
580        };
581        assert!(complex.validation_cost() > 20);
582    }
583
584    #[test]
585    fn test_schema_builders() {
586        let str_schema = Schema::string(Some(1), Some(100));
587        assert!(matches!(str_schema, Schema::String { .. }));
588
589        let int_schema = Schema::integer(Some(0), Some(100));
590        assert!(matches!(int_schema, Schema::Integer { .. }));
591
592        let arr_schema = Schema::array(Some(Schema::integer(None, None)));
593        assert!(matches!(arr_schema, Schema::Array { .. }));
594    }
595
596    /// Builds `levels` nested `Schema::Array` wrappers around a `Boolean`
597    /// leaf, iteratively (not recursively), so construction itself never
598    /// recurses on the Rust call stack regardless of `levels`.
599    fn nested_array_schema(levels: usize) -> Schema {
600        let mut schema = Schema::Boolean;
601        for _ in 0..levels {
602            schema = Schema::array(Some(schema));
603        }
604        schema
605    }
606
607    #[test]
608    fn test_schema_deserialize_at_max_depth_succeeds() {
609        let schema = nested_array_schema(MAX_DESERIALIZE_DEPTH);
610        let bytes = rmp_serde::to_vec(&schema).unwrap();
611        let result: Result<Schema, _> = rmp_serde::from_slice(&bytes);
612        assert!(result.is_ok(), "{:?}", result.err());
613    }
614
615    #[test]
616    fn test_schema_deserialize_beyond_max_depth_rejected() {
617        let schema = nested_array_schema(MAX_DESERIALIZE_DEPTH + 1);
618        let bytes = rmp_serde::to_vec(&schema).unwrap();
619        let err = rmp_serde::from_slice::<Schema>(&bytes)
620            .unwrap_err()
621            .to_string();
622        // Assert on our message, not just is_err(), so the test cannot pass
623        // because rmp-serde rejected the input for an unrelated reason.
624        assert!(err.contains("Schema nesting depth"), "{err}");
625    }
626
627    #[test]
628    fn test_schema_deserialize_small_nesting_roundtrips_via_json() {
629        let schema = nested_array_schema(3);
630        let json = serde_json::to_string(&schema).unwrap();
631        let back: Schema = serde_json::from_str(&json).unwrap();
632        assert_eq!(schema, back);
633    }
634
635    /// Over `serde_json`, `Schema`'s externally-tagged `Array` encoding costs
636    /// 2 JSON container tokens per nesting level, so `serde_json`'s own
637    /// ~128-level structural recursion limit (not this guard's own check) is
638    /// what actually fires at `Schema` depth `MAX_DESERIALIZE_DEPTH`
639    /// (64 * 2 = 128) — empirically confirmed: depth `MAX_DESERIALIZE_DEPTH -
640    /// 1` (63) deserializes successfully over JSON, while depth
641    /// `MAX_DESERIALIZE_DEPTH` (64) already fails, via `serde_json`'s own
642    /// `"recursion limit exceeded"`, before this guard's own counter (which
643    /// only rejects at 65 — see `test_schema_deserialize_beyond_max_depth_rejected`)
644    /// ever gets the chance to reject it. Only asserts `is_err()` here,
645    /// deliberately not this guard's own message text, since that message is
646    /// not what actually fires for JSON at this depth.
647    #[test]
648    fn test_schema_deserialize_json_boundary_hits_serde_json_own_limit() {
649        let ok_schema = nested_array_schema(MAX_DESERIALIZE_DEPTH - 1);
650        let json = serde_json::to_string(&ok_schema).unwrap();
651        let result: Result<Schema, _> = serde_json::from_str(&json);
652        assert!(result.is_ok(), "{:?}", result.err());
653
654        let err_schema = nested_array_schema(MAX_DESERIALIZE_DEPTH);
655        let json = serde_json::to_string(&err_schema).unwrap();
656        let result: Result<Schema, _> = serde_json::from_str(&json);
657        assert!(result.is_err());
658    }
659
660    /// `Array::items` must stay optional: `#[serde(deserialize_with = ...)]`
661    /// alone suppresses serde derive's implicit "absent `Option<T>` field ->
662    /// `None`" behavior, which would make `items` newly required and break
663    /// any persisted `Schema::Array` document that omits it. The `#[serde(default,
664    /// deserialize_with = ...)]` combination on the field restores the
665    /// pre-guard optional behavior.
666    #[test]
667    fn test_schema_array_items_still_optional_when_absent() {
668        let json = r#"{"Array":{"min_items":null,"max_items":null,"unique_items":false}}"#;
669        let schema: Schema = serde_json::from_str(json).unwrap();
670        assert!(matches!(schema, Schema::Array { items: None, .. }));
671    }
672
673    /// `Object::properties` and `OneOf::schemas` were already required
674    /// fields before the depth guard (non-`Option` types with no
675    /// `#[serde(default)]`), so adding `deserialize_with` to them — unlike
676    /// `Array::items` above — doesn't change their required-ness.
677    #[test]
678    fn test_schema_properties_and_schemas_still_required() {
679        let json = r#"{"Object":{"required":[],"additional_properties":true}}"#;
680        let result: Result<Schema, _> = serde_json::from_str(json);
681        assert!(result.is_err(), "properties should still be required");
682
683        let json = r#"{"OneOf":{}}"#;
684        let result: Result<Schema, _> = serde_json::from_str(json);
685        assert!(result.is_err(), "schemas should still be required");
686    }
687}