Skip to main content

valico/json_schema/
schema.rs

1use serde_json::Value;
2use std::borrow::Cow;
3use std::cell::{Ref, RefCell};
4use std::collections;
5use std::ops;
6use url::Url;
7
8use super::keywords;
9use super::scope;
10use super::validators;
11use super::{helpers, SchemaVersion};
12use std::error::Error;
13use std::fmt;
14use std::fmt::{Display, Formatter};
15
16#[derive(Debug)]
17pub struct WalkContext<'a> {
18    pub url: &'a Url,
19    pub fragment: Vec<String>,
20    pub scopes: &'a mut collections::HashMap<String, Vec<String>>,
21    pub version: SchemaVersion,
22}
23
24impl<'a> WalkContext<'a> {
25    pub fn escaped_fragment(&self) -> String {
26        helpers::connect(
27            self.fragment
28                .iter()
29                .map(|s| s.as_ref())
30                .collect::<Vec<&str>>()
31                .as_ref(),
32        )
33    }
34}
35
36#[derive(Debug)]
37#[allow(missing_copy_implementations)]
38pub enum SchemaError {
39    WrongId,
40    IdConflicts,
41    NotAnObject,
42    UrlParseError(url::ParseError),
43    UnknownKey(String),
44    Malformed { path: String, detail: String },
45}
46
47impl Display for SchemaError {
48    fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
49        match *self {
50            SchemaError::WrongId => write!(f, "wrong id"),
51            SchemaError::IdConflicts => write!(f, "id conflicts"),
52            SchemaError::NotAnObject => write!(f, "not an object"),
53            SchemaError::UrlParseError(ref e) => write!(f, "url parse error: {e}"),
54            SchemaError::UnknownKey(ref k) => write!(f, "unknown key: {k}"),
55            SchemaError::Malformed {
56                ref path,
57                ref detail,
58            } => write!(f, "malformed path: `{path}`, details: {detail}"),
59        }
60    }
61}
62
63impl Error for SchemaError {}
64
65#[derive(Debug)]
66pub struct ScopedSchema<'a> {
67    scope: &'a scope::Scope,
68    schema: &'a Schema,
69}
70
71impl<'a> ops::Deref for ScopedSchema<'a> {
72    type Target = Schema;
73
74    fn deref(&self) -> &Schema {
75        self.schema
76    }
77}
78
79impl<'a> ScopedSchema<'a> {
80    pub fn new(scope: &'a scope::Scope, schema: &'a Schema) -> ScopedSchema<'a> {
81        ScopedSchema { scope, schema }
82    }
83
84    pub fn validate(&self, data: &Value) -> validators::ValidationState {
85        self.schema.validate_in_scope(data, "", self.scope)
86    }
87
88    pub fn validate_in(&self, data: &Value, path: &str) -> validators::ValidationState {
89        self.schema.validate_in_scope(data, path, self.scope)
90    }
91}
92
93#[derive(Debug)]
94#[allow(dead_code)]
95pub struct Schema {
96    pub id: Option<Url>,
97    schema: Option<Url>,
98    original: Value,
99    tree: collections::BTreeMap<String, Schema>,
100    validators: validators::Validators,
101    scopes: collections::HashMap<String, Vec<String>>,
102    default: RefCell<Option<Value>>,
103}
104
105include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
106
107pub struct CompilationSettings<'a> {
108    pub keywords: &'a keywords::KeywordMap,
109    pub ban_unknown_keywords: bool,
110    pub schema_version: SchemaVersion,
111}
112
113impl<'a> CompilationSettings<'a> {
114    pub fn new(
115        keywords: &'a keywords::KeywordMap,
116        ban_unknown_keywords: bool,
117        schema_version: SchemaVersion,
118    ) -> CompilationSettings<'a> {
119        CompilationSettings {
120            keywords,
121            ban_unknown_keywords,
122            schema_version,
123        }
124    }
125}
126
127impl Schema {
128    fn compile(
129        def: Value,
130        external_id: Option<Url>,
131        settings: CompilationSettings,
132    ) -> Result<Schema, SchemaError> {
133        let def = helpers::convert_boolean_schema(def);
134
135        if !def.is_object() {
136            return Err(SchemaError::NotAnObject);
137        }
138
139        let mut id = if let Some(id) = external_id {
140            id
141        } else {
142            helpers::parse_url_key("$id", &def)?.unwrap_or_else(helpers::generate_id)
143        };
144
145        if settings.schema_version >= SchemaVersion::Draft2019_09 {
146            if let Some(anchor) = def.get("$anchor") {
147                let anchor = anchor.as_str().ok_or_else(|| SchemaError::Malformed {
148                    path: "".to_string(),
149                    detail: "$anchor must be a string".to_string(),
150                })?;
151                id.set_fragment(Some(anchor));
152            };
153        }
154
155        let schema = helpers::parse_url_key("$schema", &def)?;
156
157        let (tree, mut scopes) = {
158            let mut tree = collections::BTreeMap::new();
159            let obj = def.as_object().unwrap();
160
161            let mut scopes = collections::HashMap::new();
162
163            for (key, value) in obj.iter() {
164                if !value.is_object() && !value.is_array() && !value.is_boolean() {
165                    continue;
166                }
167                if FINAL_KEYS.contains(&key[..]) {
168                    continue;
169                }
170
171                let mut context = WalkContext {
172                    url: &id,
173                    fragment: vec![key.clone()],
174                    scopes: &mut scopes,
175                    version: settings.schema_version,
176                };
177
178                let scheme = Schema::compile_sub(
179                    value.clone(),
180                    &mut context,
181                    &settings,
182                    !NON_SCHEMA_KEYS.contains(&key[..]),
183                )?;
184
185                tree.insert(helpers::encode(key), scheme);
186            }
187
188            (tree, scopes)
189        };
190
191        let validators = Schema::compile_keywords(
192            &def,
193            &WalkContext {
194                url: &id,
195                fragment: vec![],
196                scopes: &mut scopes,
197                version: settings.schema_version,
198            },
199            &settings,
200        )?;
201
202        let schema = Schema {
203            id: Some(id),
204            schema,
205            original: def,
206            tree,
207            validators,
208            scopes,
209            default: RefCell::new(None),
210        };
211
212        Ok(schema)
213    }
214
215    /// The issue here is that it is impossible to explain
216    /// to the Rust borrow checker that I’m traversing a tree, mutating it at the same
217    /// time, and need to occasionally jump back to the root — which is safe in this
218    /// particular case because the tree structure is not touched!
219    ///
220    /// This operation is safe (i.e. it will not panic) as long as it is not called
221    /// while the default value is borrowed, hence unsafe_get_default must not be used
222    /// directly, only via `get_default()` and `has_default()`.
223    fn unsafe_set_default(&self, default: Option<Value>) {
224        self.default.replace(default);
225    }
226
227    /// Getting references to internally mutable memory is finicky business, we must
228    /// not allow these references to escape. Therefore only internal code is allowed
229    /// to use this function to
230    ///
231    ///  - take a peek at the value, to see whether the option is set, or
232    ///  - take a peek at the value just long enough to clone it.
233    fn unsafe_get_default(&self) -> Ref<Option<Value>> {
234        self.default.borrow()
235    }
236
237    pub fn get_default(&self) -> Option<Value> {
238        self.unsafe_get_default().clone()
239    }
240
241    pub fn has_default(&self) -> bool {
242        self.unsafe_get_default().is_some()
243    }
244
245    pub fn add_defaults(&mut self, id: &Url, scope: &scope::Scope) {
246        self.add_defaults_recursive(self, id, scope);
247    }
248
249    fn add_defaults_recursive(&self, top: &Schema, id: &Url, scope: &scope::Scope) {
250        // step 0: bail out if we already have a default (i.e. proof that traversal got here before)
251        if self.has_default() {
252            return;
253        }
254
255        // step 1: walk the tree to apply this recursively
256        for (_, schema) in self.tree.iter() {
257            schema.add_defaults_recursive(top, id, scope);
258        }
259
260        // step 2: use explicit default if present
261        if let Some(default) = self.original.get("default") {
262            self.unsafe_set_default(Some(default.clone()));
263            return;
264        }
265
266        // step 3: propagate defaults according to the rules
267        // 3a: $ref
268        if let Some(ref_) = self.original.get("$ref").and_then(|r| r.as_str()) {
269            if let Ok(url) = Url::options().base_url(Some(id)).parse(ref_) {
270                // first try to resolve this Url internally so that we can then modify the schema
271                // in case this one has not yet been traversed
272                if let Some(schema) = top.resolve_internal(&url) {
273                    schema.add_defaults_recursive(top, id, scope);
274                    self.unsafe_set_default(schema.get_default());
275                } else if let Some(schema) = scope.resolve(&url) {
276                    self.unsafe_set_default(schema.get_default());
277                }
278            }
279            // $ref is exclusive, i.e. does not tolerate other keywords to be present
280            return;
281        }
282        // 3b: properties
283        if let Some(properties) = self.tree.get("properties") {
284            let mut default = serde_json::Map::default();
285            for (key, schema) in properties.tree.iter() {
286                if let Some(value) = schema.get_default() {
287                    default.insert(key.clone(), value);
288                }
289            }
290            if !default.is_empty() {
291                self.unsafe_set_default(Some(default.into()));
292                return;
293            }
294        }
295        // 3c: items, if array
296        // only create a default, if there are defaults given for all items
297        if self
298            .original
299            .get("items")
300            .map(|i| i.is_array())
301            .unwrap_or(false)
302        {
303            let items = self.tree.get("items").unwrap();
304            let mut default = vec![];
305            for idx in 0.. {
306                if let Some(schema) = items.tree.get(&idx.to_string()) {
307                    if let Some(def) = schema.get_default() {
308                        default.push(def);
309                    } else {
310                        break;
311                    }
312                } else {
313                    break;
314                }
315            }
316            if default.len() == items.tree.len() {
317                self.unsafe_set_default(Some(default.into()));
318            }
319        }
320    }
321
322    fn compile_keywords(
323        def: &Value,
324        context: &WalkContext,
325        settings: &CompilationSettings,
326    ) -> Result<validators::Validators, SchemaError> {
327        let mut validators = vec![];
328        let mut end_validators = vec![];
329        let mut keys: collections::HashSet<&str> = def
330            .as_object()
331            .unwrap()
332            .keys()
333            .map(|key| key.as_ref())
334            .collect();
335        let mut not_consumed = collections::HashSet::new();
336
337        loop {
338            let key = keys.iter().next().cloned();
339            if let Some(key) = key {
340                match settings.keywords.get(&key) {
341                    Some(keyword) => {
342                        keyword.consume(&mut keys);
343
344                        let is_exclusive_keyword =
345                            keyword.keyword.is_exclusive(settings.schema_version);
346
347                        if let Some(validator) = keyword.keyword.compile(def, context)? {
348                            if is_exclusive_keyword {
349                                validators = vec![validator];
350                                end_validators = vec![];
351                            } else if keyword.keyword.place_first() {
352                                validators.splice(0..0, std::iter::once(validator));
353                            } else if keyword.keyword.place_last() {
354                                end_validators.push(validator);
355                            } else {
356                                validators.push(validator);
357                            }
358                        }
359
360                        if is_exclusive_keyword {
361                            break;
362                        }
363                    }
364                    None => {
365                        keys.remove(&key);
366                        if settings.ban_unknown_keywords {
367                            not_consumed.insert(key);
368                        }
369                    }
370                }
371            } else {
372                break;
373            }
374        }
375
376        if settings.ban_unknown_keywords && !not_consumed.is_empty() {
377            for key in not_consumed.iter() {
378                if !ALLOW_NON_CONSUMED_KEYS.contains(&key[..]) {
379                    return Err(SchemaError::UnknownKey((*key).to_string()));
380                }
381            }
382        }
383
384        validators.extend(end_validators);
385        Ok(validators)
386    }
387
388    fn compile_sub(
389        def: Value,
390        context: &mut WalkContext,
391        keywords: &CompilationSettings,
392        is_schema: bool,
393    ) -> Result<Schema, SchemaError> {
394        let def = helpers::convert_boolean_schema(def);
395
396        let id = if is_schema {
397            let mut id_url = helpers::parse_url_key_with_base("$id", &def, context.url)?;
398            if keywords.schema_version >= SchemaVersion::Draft2019_09 {
399                if let Some(anchor) = def.get("$anchor") {
400                    let anchor = anchor.as_str().ok_or_else(|| SchemaError::Malformed {
401                        path: "".to_string(),
402                        detail: "$anchor must be a string".to_string(),
403                    })?;
404
405                    // If the "$id" URL is not explicitly overridden, implicitly inherit the parent's URL.
406                    if id_url.is_none() {
407                        id_url = Some(context.url.clone());
408                    }
409
410                    id_url.as_mut().unwrap().set_fragment(Some(anchor));
411                }
412            }
413            id_url
414        } else {
415            None
416        };
417
418        let schema = if is_schema {
419            helpers::parse_url_key("$schema", &def)?
420        } else {
421            None
422        };
423
424        let tree = {
425            let mut tree = collections::BTreeMap::new();
426
427            if def.is_object() {
428                let obj = def.as_object().unwrap();
429                let parent_key = &context.fragment[context.fragment.len() - 1];
430
431                for (key, value) in obj.iter() {
432                    if !value.is_object() && !value.is_array() && !value.is_boolean() {
433                        continue;
434                    }
435                    if !PROPERTY_KEYS.contains(&parent_key[..]) && FINAL_KEYS.contains(&key[..]) {
436                        continue;
437                    }
438
439                    let mut current_fragment = context.fragment.clone();
440                    current_fragment.push(key.clone());
441
442                    let is_schema = PROPERTY_KEYS.contains(&parent_key[..])
443                        || !NON_SCHEMA_KEYS.contains(&key[..]);
444
445                    let mut context = WalkContext {
446                        url: id.as_ref().unwrap_or(context.url),
447                        fragment: current_fragment,
448                        scopes: context.scopes,
449                        version: keywords.schema_version,
450                    };
451
452                    let scheme =
453                        Schema::compile_sub(value.clone(), &mut context, keywords, is_schema)?;
454
455                    tree.insert(helpers::encode(key), scheme);
456                }
457            } else if def.is_array() {
458                let array = def.as_array().unwrap();
459                let parent_key = &context.fragment[context.fragment.len() - 1];
460
461                for (idx, value) in array.iter().enumerate() {
462                    let mut value = value.clone();
463
464                    if BOOLEAN_SCHEMA_ARRAY_KEYS.contains(&parent_key[..]) {
465                        value = helpers::convert_boolean_schema(value);
466                    }
467
468                    if !value.is_object() && !value.is_array() {
469                        continue;
470                    }
471
472                    let mut current_fragment = context.fragment.clone();
473                    current_fragment.push(idx.to_string().clone());
474
475                    let mut context = WalkContext {
476                        url: id.as_ref().unwrap_or(context.url),
477                        fragment: current_fragment,
478                        scopes: context.scopes,
479                        version: keywords.schema_version,
480                    };
481
482                    let scheme = Schema::compile_sub(value.clone(), &mut context, keywords, true)?;
483
484                    tree.insert(idx.to_string().clone(), scheme);
485                }
486            }
487
488            tree
489        };
490
491        if id.is_some() {
492            context
493                .scopes
494                .insert(id.clone().unwrap().into(), context.fragment.clone());
495        }
496
497        let validators = if is_schema && def.is_object() {
498            Schema::compile_keywords(&def, context, keywords)?
499        } else {
500            vec![]
501        };
502
503        let schema = Schema {
504            id,
505            schema,
506            original: def,
507            tree,
508            validators,
509            scopes: collections::HashMap::new(),
510            default: RefCell::new(None),
511        };
512
513        Ok(schema)
514    }
515
516    pub fn resolve(&self, id: &str) -> Option<&Schema> {
517        let path = self.scopes.get(id);
518        path.map(|path| {
519            let mut schema = self;
520            for item in path.iter() {
521                schema = &schema.tree[item]
522            }
523            schema
524        })
525    }
526
527    fn resolve_internal(&self, url: &Url) -> Option<&Schema> {
528        let (schema_path, fragment) = helpers::serialize_schema_path(url);
529        if self.id.is_some() && schema_path.as_str() == self.id.as_ref().unwrap().as_str() {
530            if let Some(fragment) = fragment {
531                self.resolve_fragment(fragment.as_str())
532            } else {
533                Some(self)
534            }
535        } else if let Some(id_path) = self.scopes.get(&schema_path) {
536            let mut schema = self;
537            for item in id_path.iter() {
538                schema = &schema.tree[item]
539            }
540            if let Some(fragment) = fragment {
541                schema.resolve_fragment(fragment.as_str())
542            } else {
543                Some(schema)
544            }
545        } else {
546            None
547        }
548    }
549
550    pub fn resolve_fragment(&self, fragment: &str) -> Option<&Schema> {
551        assert!(fragment.starts_with('/'), "Can't resolve id fragments");
552
553        let parts = fragment[1..].split('/');
554        let mut schema = self;
555        for part in parts {
556            match schema.tree.get(part) {
557                Some(sch) => schema = sch,
558                None => return None,
559            }
560        }
561
562        Some(schema)
563    }
564}
565
566impl Schema {
567    fn validate_in_scope(
568        &self,
569        data: &Value,
570        path: &str,
571        scope: &scope::Scope,
572    ) -> validators::ValidationState {
573        let mut state = validators::ValidationState::new();
574        let mut data = Cow::Borrowed(data);
575
576        for validator in self.validators.iter() {
577            let mut result = validator.validate(&data, path, scope, &state);
578            if result.is_valid() && result.replacement.is_some() {
579                *data.to_mut() = result.replacement.take().unwrap();
580            }
581            state.append(result);
582        }
583
584        state.set_replacement(data);
585        state
586    }
587}
588
589pub fn compile(
590    def: Value,
591    external_id: Option<Url>,
592    settings: CompilationSettings<'_>,
593) -> Result<Schema, SchemaError> {
594    Schema::compile(def, external_id, settings)
595}
596
597#[test]
598fn schema_doesnt_compile_not_object() {
599    assert!(Schema::compile(
600        json!(0),
601        None,
602        CompilationSettings::new(&keywords::default(), true, SchemaVersion::Draft7)
603    )
604    .is_err());
605}
606
607#[test]
608fn schema_compiles_boolean_schema() {
609    assert!(Schema::compile(
610        json!(true),
611        None,
612        CompilationSettings::new(&keywords::default(), true, SchemaVersion::Draft7)
613    )
614    .is_ok());
615}