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}