eure_json_schema/
json_schema.rs

1//! JSON Schema representation as Rust ADT
2//!
3//! This module provides a strongly-typed representation of JSON Schema (Draft-07)
4//! using Rust's algebraic data types. Each variant contains only the fields
5//! relevant to that schema type, avoiding the "bag of optional fields" anti-pattern.
6
7use indexmap::IndexMap;
8use serde::{Deserialize, Serialize};
9
10/// Common metadata fields for all schema types
11#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
12#[serde(deny_unknown_fields)]
13pub struct SchemaMetadata {
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub title: Option<String>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub description: Option<String>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub deprecated: Option<bool>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub examples: Option<Vec<serde_json::Value>>,
22}
23
24/// JSON Schema root type
25///
26/// Uses `#[serde(untagged)]` to discriminate based on the presence of specific fields.
27/// The order matters: more specific variants should come first.
28#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
29#[serde(untagged)]
30#[allow(clippy::large_enum_variant)]
31pub enum JsonSchema {
32    /// Boolean schema (true = allow all, false = deny all)
33    Boolean(bool),
34
35    /// Reference to another schema ($ref)
36    Reference(ReferenceSchema),
37
38    /// Enum constraint (fixed set of values)
39    Enum(EnumSchema),
40
41    /// Const constraint (single fixed value)
42    Const(ConstSchema),
43
44    /// AllOf composition
45    AllOf(AllOfSchema),
46
47    /// AnyOf composition
48    AnyOf(AnyOfSchema),
49
50    /// OneOf composition
51    OneOf(OneOfSchema),
52
53    /// Not composition
54    Not(NotSchema),
55
56    /// Typed schema with type-specific constraints
57    Typed(TypedSchema),
58
59    /// Generic schema (catch-all for schemas with metadata, definitions, or untyped constraints)
60    /// This includes empty schemas and schemas with only metadata
61    Generic(GenericSchema),
62}
63
64/// Reference schema ($ref)
65#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
66#[serde(deny_unknown_fields)]
67pub struct ReferenceSchema {
68    #[serde(rename = "$ref")]
69    pub reference: String,
70
71    #[serde(flatten)]
72    pub metadata: SchemaMetadata,
73}
74
75/// Enum schema (fixed set of allowed values)
76#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
77#[serde(deny_unknown_fields)]
78pub struct EnumSchema {
79    #[serde(rename = "enum")]
80    pub values: Vec<serde_json::Value>,
81
82    #[serde(flatten)]
83    pub metadata: SchemaMetadata,
84}
85
86/// Const schema (single fixed value)
87#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
88#[serde(deny_unknown_fields)]
89pub struct ConstSchema {
90    #[serde(rename = "const")]
91    pub value: serde_json::Value,
92
93    #[serde(flatten)]
94    pub metadata: SchemaMetadata,
95}
96
97/// AllOf schema (all schemas must match)
98#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
99#[serde(deny_unknown_fields)]
100pub struct AllOfSchema {
101    #[serde(rename = "allOf")]
102    pub schemas: Vec<JsonSchema>,
103
104    #[serde(flatten)]
105    pub metadata: SchemaMetadata,
106}
107
108/// AnyOf schema (at least one schema must match)
109#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
110#[serde(deny_unknown_fields)]
111pub struct AnyOfSchema {
112    #[serde(rename = "anyOf")]
113    pub schemas: Vec<JsonSchema>,
114
115    #[serde(flatten)]
116    pub metadata: SchemaMetadata,
117}
118
119/// OneOf schema (exactly one schema must match)
120#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
121#[serde(deny_unknown_fields)]
122pub struct OneOfSchema {
123    #[serde(rename = "oneOf")]
124    pub schemas: Vec<JsonSchema>,
125
126    #[serde(flatten)]
127    pub metadata: SchemaMetadata,
128}
129
130/// Not schema (must not match the schema)
131#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
132#[serde(deny_unknown_fields)]
133pub struct NotSchema {
134    pub not: Box<JsonSchema>,
135
136    #[serde(flatten)]
137    pub metadata: SchemaMetadata,
138}
139
140/// Typed schema (discriminated by "type" field)
141#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
142#[serde(tag = "type", deny_unknown_fields)]
143pub enum TypedSchema {
144    #[serde(rename = "string")]
145    String(StringSchema),
146
147    #[serde(rename = "number")]
148    Number(NumberSchema),
149
150    #[serde(rename = "integer")]
151    Integer(IntegerSchema),
152
153    #[serde(rename = "boolean")]
154    Boolean(BooleanSchema),
155
156    #[serde(rename = "null")]
157    Null(NullSchema),
158
159    #[serde(rename = "array")]
160    Array(ArraySchema),
161
162    #[serde(rename = "object")]
163    Object(ObjectSchema),
164}
165
166/// String type schema
167#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
168#[serde(deny_unknown_fields)]
169pub struct StringSchema {
170    #[serde(rename = "minLength", skip_serializing_if = "Option::is_none")]
171    pub min_length: Option<u32>,
172
173    #[serde(rename = "maxLength", skip_serializing_if = "Option::is_none")]
174    pub max_length: Option<u32>,
175
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub pattern: Option<String>,
178
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub format: Option<String>,
181
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub default: Option<String>,
184
185    #[serde(flatten)]
186    pub metadata: SchemaMetadata,
187}
188
189/// Number type schema (floating point)
190#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
191#[serde(deny_unknown_fields)]
192pub struct NumberSchema {
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub minimum: Option<f64>,
195
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub maximum: Option<f64>,
198
199    #[serde(rename = "exclusiveMinimum", skip_serializing_if = "Option::is_none")]
200    pub exclusive_minimum: Option<f64>,
201
202    #[serde(rename = "exclusiveMaximum", skip_serializing_if = "Option::is_none")]
203    pub exclusive_maximum: Option<f64>,
204
205    #[serde(rename = "multipleOf", skip_serializing_if = "Option::is_none")]
206    pub multiple_of: Option<f64>,
207
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub default: Option<f64>,
210
211    #[serde(flatten)]
212    pub metadata: SchemaMetadata,
213}
214
215/// Integer type schema
216#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
217#[serde(deny_unknown_fields)]
218pub struct IntegerSchema {
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub minimum: Option<i64>,
221
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub maximum: Option<i64>,
224
225    #[serde(rename = "exclusiveMinimum", skip_serializing_if = "Option::is_none")]
226    pub exclusive_minimum: Option<i64>,
227
228    #[serde(rename = "exclusiveMaximum", skip_serializing_if = "Option::is_none")]
229    pub exclusive_maximum: Option<i64>,
230
231    #[serde(rename = "multipleOf", skip_serializing_if = "Option::is_none")]
232    pub multiple_of: Option<i64>,
233
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub default: Option<i64>,
236
237    #[serde(flatten)]
238    pub metadata: SchemaMetadata,
239}
240
241/// Boolean type schema
242#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
243#[serde(deny_unknown_fields)]
244pub struct BooleanSchema {
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub default: Option<bool>,
247
248    #[serde(flatten)]
249    pub metadata: SchemaMetadata,
250}
251
252/// Null type schema
253#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
254#[serde(deny_unknown_fields)]
255pub struct NullSchema {
256    #[serde(flatten)]
257    pub metadata: SchemaMetadata,
258}
259
260/// Array type schema
261#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
262#[serde(deny_unknown_fields)]
263pub struct ArraySchema {
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub items: Option<Box<JsonSchema>>,
266
267    /// Tuple validation: each element must match the corresponding schema (JSON Schema 2020-12)
268    /// In Draft-07, this was called "items" with an array value, but we use prefixItems for clarity
269    #[serde(rename = "prefixItems", skip_serializing_if = "Option::is_none")]
270    pub prefix_items: Option<Vec<JsonSchema>>,
271
272    #[serde(rename = "minItems", skip_serializing_if = "Option::is_none")]
273    pub min_items: Option<u32>,
274
275    #[serde(rename = "maxItems", skip_serializing_if = "Option::is_none")]
276    pub max_items: Option<u32>,
277
278    #[serde(rename = "uniqueItems", skip_serializing_if = "Option::is_none")]
279    pub unique_items: Option<bool>,
280
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub contains: Option<Box<JsonSchema>>,
283
284    #[serde(flatten)]
285    pub metadata: SchemaMetadata,
286}
287
288/// Object type schema (with explicit type: "object")
289#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
290#[serde(deny_unknown_fields)]
291pub struct ObjectSchema {
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub properties: Option<IndexMap<String, JsonSchema>>,
294
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub required: Option<Vec<String>>,
297
298    #[serde(
299        rename = "additionalProperties",
300        skip_serializing_if = "Option::is_none"
301    )]
302    pub additional_properties: Option<AdditionalProperties>,
303
304    // Note: patternProperties is not yet supported in this ADT
305    // It would require more complex handling
306    #[serde(flatten)]
307    pub metadata: SchemaMetadata,
308}
309
310/// Additional properties policy
311#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
312#[serde(untagged)]
313pub enum AdditionalProperties {
314    Bool(bool),
315    Schema(Box<JsonSchema>),
316}
317
318/// Generic schema (catch-all)
319/// This handles schemas without explicit type, including:
320/// - Empty schemas {}
321/// - Schemas with only metadata
322/// - Schemas with object-specific fields but no type
323/// - Schemas with definitions
324#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
325#[serde(deny_unknown_fields)]
326pub struct GenericSchema {
327    // Object-like fields (without explicit type)
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub properties: Option<IndexMap<String, JsonSchema>>,
330    #[serde(skip_serializing_if = "Option::is_none")]
331    pub required: Option<Vec<String>>,
332
333    #[serde(
334        rename = "additionalProperties",
335        skip_serializing_if = "Option::is_none"
336    )]
337    pub additional_properties: Option<AdditionalProperties>,
338
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub default: Option<serde_json::Value>,
341
342    // Schema metadata
343    #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
344    pub schema: Option<String>,
345
346    #[serde(rename = "$id", skip_serializing_if = "Option::is_none")]
347    pub id: Option<String>,
348
349    #[serde(rename = "$defs", skip_serializing_if = "Option::is_none")]
350    pub defs: Option<IndexMap<String, JsonSchema>>,
351
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub definitions: Option<IndexMap<String, JsonSchema>>,
354
355    #[serde(flatten)]
356    pub metadata: SchemaMetadata,
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362
363    #[test]
364    fn test_parse_boolean_schema_true() {
365        let json = "true";
366        let schema: JsonSchema = serde_json::from_str(json).unwrap();
367        match schema {
368            JsonSchema::Boolean(true) => {}
369            _ => panic!("Expected Boolean(true)"),
370        }
371    }
372
373    #[test]
374    fn test_parse_boolean_schema_false() {
375        let json = "false";
376        let schema: JsonSchema = serde_json::from_str(json).unwrap();
377        match schema {
378            JsonSchema::Boolean(false) => {}
379            _ => panic!("Expected Boolean(false)"),
380        }
381    }
382
383    #[test]
384    fn test_parse_simple_string_schema() {
385        let json = r#"{"type": "string"}"#;
386        let schema: JsonSchema = serde_json::from_str(json).unwrap();
387        assert_eq!(
388            schema,
389            JsonSchema::Typed(TypedSchema::String(StringSchema {
390                min_length: None,
391                max_length: None,
392                pattern: None,
393                format: None,
394                default: None,
395                metadata: SchemaMetadata::default(),
396            }))
397        );
398    }
399
400    #[test]
401    fn test_parse_string_with_constraints() {
402        let json = r#"{
403            "type": "string",
404            "minLength": 3,
405            "maxLength": 20,
406            "pattern": "^[a-z]+$"
407        }"#;
408        let schema: JsonSchema = serde_json::from_str(json).unwrap();
409        assert_eq!(
410            schema,
411            JsonSchema::Typed(TypedSchema::String(StringSchema {
412                min_length: Some(3),
413                max_length: Some(20),
414                pattern: Some("^[a-z]+$".to_string()),
415                format: None,
416                default: None,
417                metadata: SchemaMetadata::default(),
418            }))
419        );
420    }
421
422    #[test]
423    fn test_parse_integer_schema() {
424        let json = r#"{
425            "type": "integer",
426            "minimum": 0,
427            "maximum": 100
428        }"#;
429        let schema: JsonSchema = serde_json::from_str(json).unwrap();
430        match schema {
431            JsonSchema::Typed(TypedSchema::Integer(i)) => {
432                assert_eq!(i.minimum, Some(0));
433                assert_eq!(i.maximum, Some(100));
434            }
435            _ => panic!("Expected Typed(Integer)"),
436        }
437    }
438
439    #[test]
440    fn test_parse_number_schema() {
441        let json = r#"{
442            "type": "number",
443            "minimum": 0.0,
444            "exclusiveMaximum": 1.0
445        }"#;
446        let schema: JsonSchema = serde_json::from_str(json).unwrap();
447        match schema {
448            JsonSchema::Typed(TypedSchema::Number(n)) => {
449                assert_eq!(n.minimum, Some(0.0));
450                assert_eq!(n.exclusive_maximum, Some(1.0));
451            }
452            _ => panic!("Expected Typed(Number)"),
453        }
454    }
455
456    #[test]
457    fn test_parse_array_schema() {
458        let json = r#"{
459            "type": "array",
460            "items": {"type": "string"},
461            "minItems": 1,
462            "uniqueItems": true
463        }"#;
464        let schema: JsonSchema = serde_json::from_str(json).unwrap();
465        match schema {
466            JsonSchema::Typed(TypedSchema::Array(a)) => {
467                assert_eq!(a.min_items, Some(1));
468                assert_eq!(a.unique_items, Some(true));
469                assert!(a.items.is_some());
470            }
471            _ => panic!("Expected Typed(Array)"),
472        }
473    }
474
475    #[test]
476    fn test_parse_object_with_properties() {
477        let json = r#"{
478            "type": "object",
479            "properties": {
480                "name": {"type": "string"},
481                "age": {"type": "integer"}
482            },
483            "required": ["name"]
484        }"#;
485        let schema: JsonSchema = serde_json::from_str(json).unwrap();
486        match schema {
487            JsonSchema::Typed(TypedSchema::Object(o)) => {
488                assert!(o.properties.is_some());
489                let props = o.properties.unwrap();
490                assert_eq!(props.len(), 2);
491                assert!(props.contains_key("name"));
492                assert!(props.contains_key("age"));
493                assert_eq!(o.required, Some(vec!["name".to_string()]));
494            }
495            _ => panic!("Expected Typed(Object)"),
496        }
497    }
498
499    #[test]
500    fn test_parse_object_with_additional_properties_bool() {
501        let json = r#"{
502            "type": "object",
503            "additionalProperties": false
504        }"#;
505        let schema: JsonSchema = serde_json::from_str(json).unwrap();
506        match schema {
507            JsonSchema::Typed(TypedSchema::Object(o)) => match o.additional_properties {
508                Some(AdditionalProperties::Bool(false)) => {}
509                _ => panic!("Expected additionalProperties: false"),
510            },
511            _ => panic!("Expected Typed(Object)"),
512        }
513    }
514
515    #[test]
516    fn test_parse_object_with_additional_properties_schema() {
517        let json = r#"{
518            "type": "object",
519            "additionalProperties": {"type": "string"}
520        }"#;
521        let schema: JsonSchema = serde_json::from_str(json).unwrap();
522        match schema {
523            JsonSchema::Typed(TypedSchema::Object(o)) => match o.additional_properties {
524                Some(AdditionalProperties::Schema(_)) => {}
525                _ => panic!("Expected additionalProperties with schema"),
526            },
527            _ => panic!("Expected Typed(Object)"),
528        }
529    }
530
531    #[test]
532    fn test_parse_reference_schema() {
533        let json = r##"{"$ref": "#/definitions/User"}"##;
534        let schema: JsonSchema = serde_json::from_str(json).unwrap();
535        match schema {
536            JsonSchema::Reference(r) => {
537                assert_eq!(r.reference, "#/definitions/User");
538            }
539            _ => panic!("Expected Reference"),
540        }
541    }
542
543    #[test]
544    fn test_parse_enum_schema() {
545        let json = r#"{"enum": ["red", "green", "blue"]}"#;
546        let schema: JsonSchema = serde_json::from_str(json).unwrap();
547        match schema {
548            JsonSchema::Enum(e) => {
549                assert_eq!(e.values.len(), 3);
550            }
551            _ => panic!("Expected Enum"),
552        }
553    }
554
555    #[test]
556    fn test_parse_const_schema() {
557        let json = r#"{"const": "fixed-value"}"#;
558        let schema: JsonSchema = serde_json::from_str(json).unwrap();
559        match schema {
560            JsonSchema::Const(c) => {
561                assert_eq!(c.value, serde_json::json!("fixed-value"));
562            }
563            _ => panic!("Expected Const"),
564        }
565    }
566
567    #[test]
568    fn test_parse_anyof_schema() {
569        let json = r#"{
570            "anyOf": [
571                {"type": "string"},
572                {"type": "number"}
573            ]
574        }"#;
575        let schema: JsonSchema = serde_json::from_str(json).unwrap();
576        match schema {
577            JsonSchema::AnyOf(a) => {
578                assert_eq!(a.schemas.len(), 2);
579            }
580            _ => panic!("Expected AnyOf"),
581        }
582    }
583
584    #[test]
585    fn test_parse_allof_schema() {
586        let json = r#"{
587            "allOf": [
588                {"type": "object"},
589                {"properties": {"name": {"type": "string"}}}
590            ]
591        }"#;
592        let schema: JsonSchema = serde_json::from_str(json).unwrap();
593        match schema {
594            JsonSchema::AllOf(a) => {
595                assert_eq!(a.schemas.len(), 2);
596            }
597            other => panic!("Expected AllOf, got: {:?}", other),
598        }
599    }
600
601    #[test]
602    fn test_parse_empty_schema() {
603        let json = r#"{}"#;
604        let schema: JsonSchema = serde_json::from_str(json).unwrap();
605        match schema {
606            JsonSchema::Generic(g) => {
607                // Empty schema is parsed as Generic with all fields None
608                assert!(g.properties.is_none());
609                assert!(g.metadata.title.is_none());
610            }
611            _ => panic!("Expected Generic"),
612        }
613    }
614
615    #[test]
616    fn test_parse_empty_schema_with_metadata() {
617        let json = r#"{
618            "title": "My Schema",
619            "description": "A test schema"
620        }"#;
621        let schema: JsonSchema = serde_json::from_str(json).unwrap();
622        match schema {
623            JsonSchema::Generic(g) => {
624                assert_eq!(g.metadata.title, Some("My Schema".to_string()));
625                assert_eq!(g.metadata.description, Some("A test schema".to_string()));
626            }
627            _ => panic!("Expected Generic"),
628        }
629    }
630
631    #[test]
632    #[should_panic]
633    fn test_reject_unknown_field_in_string_schema() {
634        let json = r#"{
635            "type": "string",
636            "unknownField": "value"
637        }"#;
638        let _schema: JsonSchema = serde_json::from_str(json).unwrap();
639    }
640
641    #[test]
642    #[should_panic]
643    fn test_reject_unknown_field_in_object_schema() {
644        let json = r#"{
645            "type": "object",
646            "invalidField": true
647        }"#;
648        let _schema: JsonSchema = serde_json::from_str(json).unwrap();
649    }
650
651    #[test]
652    fn test_roundtrip_string_schema() {
653        let original = JsonSchema::Typed(TypedSchema::String(StringSchema {
654            min_length: Some(5),
655            max_length: Some(100),
656            pattern: Some("^[A-Z]".to_string()),
657            format: Some("email".to_string()),
658            default: None,
659            metadata: SchemaMetadata {
660                title: Some("Email".to_string()),
661                description: Some("User email address".to_string()),
662                deprecated: None,
663                examples: None,
664            },
665        }));
666
667        // Serialize to JSON
668        let json = serde_json::to_string(&original).unwrap();
669
670        // Deserialize back
671        let deserialized: JsonSchema = serde_json::from_str(&json).unwrap();
672
673        // Should be equal
674        assert_eq!(original, deserialized);
675    }
676
677    #[test]
678    fn test_roundtrip_object_schema() {
679        let mut properties = IndexMap::new();
680        properties.insert(
681            "name".to_string(),
682            JsonSchema::Typed(TypedSchema::String(StringSchema {
683                min_length: Some(1),
684                max_length: None,
685                pattern: None,
686                format: None,
687                default: None,
688                metadata: SchemaMetadata::default(),
689            })),
690        );
691        properties.insert(
692            "age".to_string(),
693            JsonSchema::Typed(TypedSchema::Integer(IntegerSchema {
694                minimum: Some(0),
695                maximum: Some(150),
696                exclusive_minimum: None,
697                exclusive_maximum: None,
698                multiple_of: None,
699                default: None,
700                metadata: SchemaMetadata::default(),
701            })),
702        );
703
704        let original = JsonSchema::Typed(TypedSchema::Object(ObjectSchema {
705            properties: Some(properties),
706            required: Some(vec!["name".to_string()]),
707            additional_properties: Some(AdditionalProperties::Bool(false)),
708            metadata: SchemaMetadata::default(),
709        }));
710
711        // Serialize to JSON
712        let json = serde_json::to_string(&original).unwrap();
713
714        // Deserialize back
715        let deserialized: JsonSchema = serde_json::from_str(&json).unwrap();
716
717        // Should be equal
718        assert_eq!(original, deserialized);
719    }
720
721    #[test]
722    fn test_roundtrip_anyof_schema() {
723        let original = JsonSchema::AnyOf(AnyOfSchema {
724            schemas: vec![
725                JsonSchema::Typed(TypedSchema::String(StringSchema::default())),
726                JsonSchema::Typed(TypedSchema::Integer(IntegerSchema::default())),
727            ],
728            metadata: SchemaMetadata::default(),
729        });
730
731        // Serialize to JSON
732        let json = serde_json::to_string(&original).unwrap();
733
734        // Deserialize back
735        let deserialized: JsonSchema = serde_json::from_str(&json).unwrap();
736
737        // Should be equal
738        assert_eq!(original, deserialized);
739    }
740
741    #[test]
742    fn test_parse_boolean_type_schema() {
743        let json = r#"{"type": "boolean"}"#;
744        let schema: JsonSchema = serde_json::from_str(json).unwrap();
745        match schema {
746            JsonSchema::Typed(TypedSchema::Boolean(_)) => {}
747            _ => panic!("Expected Typed(Boolean)"),
748        }
749    }
750
751    #[test]
752    fn test_parse_null_type_schema() {
753        let json = r#"{"type": "null"}"#;
754        let schema: JsonSchema = serde_json::from_str(json).unwrap();
755        match schema {
756            JsonSchema::Typed(TypedSchema::Null(_)) => {}
757            _ => panic!("Expected Typed(Null)"),
758        }
759    }
760
761    #[test]
762    fn test_parse_oneof_schema() {
763        let json = r#"{
764            "oneOf": [
765                {"type": "string"},
766                {"type": "number"}
767            ]
768        }"#;
769        let schema: JsonSchema = serde_json::from_str(json).unwrap();
770        match schema {
771            JsonSchema::OneOf(o) => {
772                assert_eq!(o.schemas.len(), 2);
773            }
774            _ => panic!("Expected OneOf"),
775        }
776    }
777
778    #[test]
779    fn test_parse_not_schema() {
780        let json = r#"{
781            "not": {"type": "string"}
782        }"#;
783        let schema: JsonSchema = serde_json::from_str(json).unwrap();
784        match schema {
785            JsonSchema::Not(n) => {
786                assert!(matches!(*n.not, JsonSchema::Typed(TypedSchema::String(_))));
787            }
788            _ => panic!("Expected Not"),
789        }
790    }
791
792    #[test]
793    fn test_parse_generic_schema_with_properties() {
794        let json = r#"{
795            "properties": {
796                "name": {"type": "string"}
797            }
798        }"#;
799        let schema: JsonSchema = serde_json::from_str(json).unwrap();
800        match schema {
801            JsonSchema::Generic(g) => {
802                assert!(g.properties.is_some());
803                let props = g.properties.unwrap();
804                assert_eq!(props.len(), 1);
805                assert!(props.contains_key("name"));
806            }
807            _ => panic!("Expected Generic"),
808        }
809    }
810
811    #[test]
812    fn test_parse_nested_allof() {
813        let json = r#"{
814            "allOf": [
815                {
816                    "allOf": [
817                        {"type": "object"},
818                        {"properties": {"id": {"type": "string"}}}
819                    ]
820                },
821                {"properties": {"name": {"type": "string"}}}
822            ]
823        }"#;
824        let schema: JsonSchema = serde_json::from_str(json).unwrap();
825        match schema {
826            JsonSchema::AllOf(a) => {
827                assert_eq!(a.schemas.len(), 2);
828                // First element should also be AllOf
829                assert!(matches!(a.schemas[0], JsonSchema::AllOf(_)));
830            }
831            _ => panic!("Expected AllOf"),
832        }
833    }
834
835    #[test]
836    fn test_roundtrip_with_metadata() {
837        let original = JsonSchema::Typed(TypedSchema::Integer(IntegerSchema {
838            minimum: Some(0),
839            maximum: Some(100),
840            exclusive_minimum: None,
841            exclusive_maximum: None,
842            multiple_of: None,
843            default: Some(50),
844            metadata: SchemaMetadata {
845                title: Some("Age".to_string()),
846                description: Some("User's age in years".to_string()),
847                deprecated: None,
848                examples: None,
849            },
850        }));
851
852        let json = serde_json::to_string(&original).unwrap();
853        let deserialized: JsonSchema = serde_json::from_str(&json).unwrap();
854
855        assert_eq!(original, deserialized);
856    }
857
858    #[test]
859    fn test_parse_array_with_contains() {
860        let json = r#"{
861            "type": "array",
862            "contains": {"type": "string"}
863        }"#;
864        let schema: JsonSchema = serde_json::from_str(json).unwrap();
865        match schema {
866            JsonSchema::Typed(TypedSchema::Array(a)) => {
867                assert!(a.contains.is_some());
868            }
869            _ => panic!("Expected Typed(Array)"),
870        }
871    }
872
873    #[test]
874    fn test_parse_integer_with_multiple_of() {
875        let json = r#"{
876            "type": "integer",
877            "multipleOf": 5
878        }"#;
879        let schema: JsonSchema = serde_json::from_str(json).unwrap();
880        match schema {
881            JsonSchema::Typed(TypedSchema::Integer(i)) => {
882                assert_eq!(i.multiple_of, Some(5));
883            }
884            _ => panic!("Expected Typed(Integer)"),
885        }
886    }
887
888    #[test]
889    fn test_parse_string_with_format() {
890        let json = r#"{
891            "type": "string",
892            "format": "email"
893        }"#;
894        let schema: JsonSchema = serde_json::from_str(json).unwrap();
895        match schema {
896            JsonSchema::Typed(TypedSchema::String(s)) => {
897                assert_eq!(s.format, Some("email".to_string()));
898            }
899            _ => panic!("Expected Typed(String)"),
900        }
901    }
902
903    #[test]
904    fn test_roundtrip_reference_with_metadata() {
905        let original = JsonSchema::Reference(ReferenceSchema {
906            reference: "#/definitions/User".to_string(),
907            metadata: SchemaMetadata {
908                title: Some("User Reference".to_string()),
909                description: Some("Reference to User type".to_string()),
910                deprecated: None,
911                examples: None,
912            },
913        });
914
915        let json = serde_json::to_string(&original).unwrap();
916        let deserialized: JsonSchema = serde_json::from_str(&json).unwrap();
917
918        assert_eq!(original, deserialized);
919    }
920}