Skip to main content

jsonschema_schema/schema/
mod.rs

1use alloc::collections::BTreeMap;
2
3use combine_structs::combine_fields;
4use indexmap::IndexMap;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use serde_json::{Number, Value};
8use url::Url;
9
10use crate::extensions::IntellijSchemaExt;
11use crate::extensions::LintelSchemaExt;
12use crate::extensions::TaploInfoSchemaExt;
13use crate::extensions::TaploSchemaExt;
14use crate::extensions::TombiSchemaExt;
15
16mod add;
17mod navigate;
18pub mod vocabularies;
19
20#[cfg(test)]
21#[allow(clippy::unwrap_used)]
22mod tests;
23
24pub use navigate::{navigate_pointer, ref_name, resolve_ref};
25
26/// Helper for `#[serde(skip_serializing_if)]` on `bool` fields.
27#[allow(clippy::trivially_copy_pass_by_ref)] // serde skip_serializing_if requires &T
28pub(crate) fn is_false(v: &bool) -> bool {
29    !v
30}
31
32/// A JSON Schema value — either a boolean schema or an object schema.
33///
34/// A schema can be a JSON object or a JSON boolean. Boolean schemas are
35/// equivalent to certain object schemas:
36///
37/// - `true` — always validates successfully (equivalent to `{}`).
38/// - `false` — never validates successfully (equivalent to `{"not": {}}`).
39///
40/// The `Other` variant catches values that are neither booleans nor valid
41/// schema objects (e.g. bare strings injected by buggy generators).  It
42/// is treated identically to `Bool(false)` by [`as_schema`](Self::as_schema).
43///
44/// See [JSON Schema Core §4.3.2](https://json-schema.org/draft/2020-12/json-schema-core#section-4.3.2).
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
46#[serde(untagged)]
47pub enum SchemaValue {
48    /// A boolean schema: `true` accepts everything, `false` rejects everything.
49    Bool(bool),
50    /// An object schema with keyword-based constraints.
51    Schema(Box<Schema>),
52    /// Catch-all for invalid schema values (strings, numbers, etc.).
53    Other(Value),
54}
55
56/// Primitive type names defined by JSON Schema (`simpleTypes`).
57///
58/// String values MUST be one of the six primitive types (`"null"`,
59/// `"boolean"`, `"object"`, `"array"`, `"number"`, or `"string"`), or
60/// `"integer"` which matches any number with a zero fractional part.
61///
62/// See [JSON Schema Validation §6.1.1](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.1.1).
63#[derive(
64    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, strum::Display,
65)]
66#[serde(rename_all = "camelCase")]
67#[strum(serialize_all = "lowercase")]
68pub enum SimpleType {
69    /// A JSON array (ordered sequence of values).
70    Array,
71    /// A JSON `true` or `false` value.
72    Boolean,
73    /// A JSON number with a zero fractional part (subset of `Number`).
74    Integer,
75    /// The JSON `null` value.
76    Null,
77    /// A JSON number (any numeric value, including integers).
78    Number,
79    /// A JSON object (unordered set of name/value pairs).
80    Object,
81    /// A JSON string.
82    String,
83}
84
85/// The value of the JSON Schema `type` keyword.
86///
87/// The value of this keyword MUST be either a string or an array. If it is
88/// an array, elements of the array MUST be strings and MUST be unique.
89///
90/// See [JSON Schema Validation §6.1.1](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.1.1).
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
92#[serde(untagged)]
93pub enum TypeValue {
94    /// A single type constraint, e.g. `"type": "string"`.
95    Single(SimpleType),
96    /// A union of types, e.g. `"type": ["string", "null"]`.
97    /// The array SHOULD have at least one element, and elements MUST be unique.
98    Union(Vec<SimpleType>),
99}
100
101// ---------------------------------------------------------------------------
102// Schema struct — generated by merging all vocabulary fields
103// ---------------------------------------------------------------------------
104
105/// A JSON Schema object (draft 2020-12).
106///
107/// Represents a single schema resource as defined by the
108/// [JSON Schema Core](https://json-schema.org/draft/2020-12/json-schema-core) and
109/// [JSON Schema Validation](https://json-schema.org/draft/2020-12/json-schema-validation)
110/// specifications.
111///
112/// Fields are grouped by vocabulary:
113///
114/// - **Core** (`$schema`, `$id`, `$ref`, `$anchor`, `$dynamicRef`,
115///   `$dynamicAnchor`, `$comment`, `$defs`, `$vocabulary`)
116/// - **Metadata / Annotation** (`title`, `description`, `default`,
117///   `deprecated`, `readOnly`, `writeOnly`, `examples`)
118/// - **Validation — type** (`type`, `enum`, `const`)
119/// - **Applicator — object** (`properties`, `patternProperties`,
120///   `additionalProperties`, `propertyNames`, `unevaluatedProperties`)
121/// - **Validation — object** (`required`, `minProperties`,
122///   `maxProperties`, `dependentRequired`)
123/// - **Applicator — array** (`items`, `prefixItems`, `contains`,
124///   `unevaluatedItems`)
125/// - **Validation — array** (`minItems`, `maxItems`, `uniqueItems`,
126///   `minContains`, `maxContains`)
127/// - **Validation — number** (`minimum`, `maximum`, `exclusiveMinimum`,
128///   `exclusiveMaximum`, `multipleOf`)
129/// - **Validation — string** (`minLength`, `maxLength`, `pattern`, `format`)
130/// - **Applicator — composition** (`allOf`, `anyOf`, `oneOf`, `not`)
131/// - **Applicator — conditional** (`if`, `then`, `else`,
132///   `dependentSchemas`)
133/// - **Content** (`contentMediaType`, `contentEncoding`, `contentSchema`)
134#[combine_fields(
135    CoreVocabulary,
136    ApplicatorVocabulary,
137    UnevaluatedVocabulary,
138    ValidationVocabulary,
139    MetaDataVocabulary,
140    FormatAnnotationVocabulary,
141    ContentVocabulary
142)]
143#[allow(clippy::struct_excessive_bools)] // mirrors the JSON Schema spec
144#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
145pub struct Schema {
146    /// The `markdownDescription` keyword — Markdown-formatted
147    /// description (VS Code / non-standard extension).
148    ///
149    /// Not part of the JSON Schema specification. When present, it is
150    /// preferred over [`description`](Self::description) by editors
151    /// that support Markdown rendering.
152    #[serde(
153        rename = "markdownDescription",
154        skip_serializing_if = "Option::is_none"
155    )]
156    pub markdown_description: Option<String>,
157
158    /// Per-enum-value Markdown descriptions (VS Code / non-standard extension).
159    #[serde(
160        rename = "markdownEnumDescriptions",
161        skip_serializing_if = "Option::is_none"
162    )]
163    pub markdown_enum_descriptions: Option<Vec<Option<String>>>,
164
165    /// Lintel provenance metadata (`x-lintel`).
166    #[serde(rename = "x-lintel", skip_serializing_if = "Option::is_none")]
167    pub x_lintel: Option<LintelSchemaExt>,
168
169    /// Taplo TOML toolkit extension (`x-taplo`).
170    #[serde(rename = "x-taplo", skip_serializing_if = "Option::is_none")]
171    pub x_taplo: Option<TaploSchemaExt>,
172    /// Taplo informational metadata (`x-taplo-info`).
173    #[serde(rename = "x-taplo-info", skip_serializing_if = "Option::is_none")]
174    pub x_taplo_info: Option<TaploInfoSchemaExt>,
175    /// Tombi TOML extensions (`x-tombi-*`).
176    #[serde(flatten)]
177    pub x_tombi: TombiSchemaExt,
178    /// `IntelliJ` IDEA extensions (`x-intellij-*`).
179    #[serde(flatten)]
180    pub x_intellij: IntellijSchemaExt,
181
182    /// Unknown or unsupported properties.
183    ///
184    /// Any JSON property that is not recognized as a standard keyword
185    /// or known extension is captured here, preserving round-trip
186    /// fidelity.
187    #[serde(flatten)]
188    pub extra: BTreeMap<String, Value>,
189}
190
191// ---------------------------------------------------------------------------
192// Impl blocks
193// ---------------------------------------------------------------------------
194
195impl SchemaValue {
196    /// Get the inner `Schema` if this is an object schema, `None` for bool
197    /// schemas and invalid (`Other`) values.
198    pub fn as_schema(&self) -> Option<&Schema> {
199        match self {
200            Self::Schema(s) => Some(s),
201            Self::Bool(_) | Self::Other(_) => None,
202        }
203    }
204}
205
206impl Schema {
207    /// Parse from a `serde_json::Value` without migration.
208    ///
209    /// # Errors
210    ///
211    /// Returns an error if the value cannot be deserialized into a `Schema`.
212    pub fn from_value(value: Value) -> Result<Self, serde_json::Error> {
213        serde_json::from_value(value)
214    }
215
216    /// Get the best description text, preferring `markdownDescription`.
217    pub fn description(&self) -> Option<&str> {
218        self.markdown_description
219            .as_deref()
220            .or(self.description.as_deref())
221    }
222
223    /// Get the required fields, or an empty slice.
224    pub fn required_set(&self) -> &[String] {
225        self.required.as_deref().unwrap_or_default()
226    }
227
228    /// Whether this schema is deprecated.
229    pub fn is_deprecated(&self) -> bool {
230        self.deprecated
231    }
232
233    /// Produce a short human-readable type string.
234    pub fn type_str(&self) -> Option<String> {
235        schema_type_str(self)
236    }
237
238    /// Validate structural integrity of this schema.
239    ///
240    /// Recursively walks the schema tree and checks that all local `$ref`
241    /// pointers (starting with `#/`) resolve to valid targets.
242    pub fn validate(&self) -> Vec<crate::validate::SchemaError> {
243        crate::validate::validate(self)
244    }
245
246    /// Rewrite all local `$ref` pointers (`#/…`) to absolute URLs using the
247    /// schema's `$id` as base.  Returns the schema unchanged if `$id` is absent.
248    #[must_use]
249    pub fn absolute(&self) -> Schema {
250        crate::absolute::make_absolute(self)
251    }
252
253    /// Flatten composition keywords (currently `allOf`) into a single merged schema.
254    ///
255    /// Properties from `allOf` entries are merged into the root, and unreferenced
256    /// `$defs` entries are pruned. The `allOf` array is preserved so provenance
257    /// remains visible.
258    #[must_use]
259    pub fn flatten(&self, root: &SchemaValue) -> Schema {
260        crate::flatten::flatten_all_of(self, root)
261    }
262
263    /// Look up a schema-keyword field by its JSON key name.
264    ///
265    /// Returns a reference to the `SchemaValue` stored under that keyword,
266    /// or `None` if the field is absent.
267    pub fn get_keyword(&self, key: &str) -> Option<&SchemaValue> {
268        match key {
269            "items" => self.items.as_deref(),
270            "contains" => self.contains.as_deref(),
271            "additionalProperties" => self.additional_properties.as_deref(),
272            "propertyNames" => self.property_names.as_deref(),
273            "unevaluatedProperties" => self.unevaluated_properties.as_deref(),
274            "unevaluatedItems" => self.unevaluated_items.as_deref(),
275            "not" => self.not.as_deref(),
276            "if" => self.if_.as_deref(),
277            "then" => self.then_.as_deref(),
278            "else" => self.else_.as_deref(),
279            "contentSchema" => self.content_schema.as_deref(),
280            _ => None,
281        }
282    }
283
284    /// Look up a named child within a keyword that holds a map of schemas.
285    ///
286    /// For example, `get_map_entry("properties", "name")` returns the schema
287    /// for the `name` property.
288    pub fn get_map_entry(&self, keyword: &str, key: &str) -> Option<&SchemaValue> {
289        match keyword {
290            "properties" => self.properties.get(key),
291            "patternProperties" => self.pattern_properties.get(key),
292            "$defs" => self.defs.as_ref()?.get(key),
293            "dependentSchemas" => self.dependent_schemas.get(key),
294            _ => None,
295        }
296    }
297
298    /// Look up an indexed child within a keyword that holds an array of schemas.
299    pub fn get_array_entry(&self, keyword: &str, index: usize) -> Option<&SchemaValue> {
300        match keyword {
301            "allOf" => self.all_of.as_ref()?.get(index),
302            "anyOf" => self.any_of.as_ref()?.get(index),
303            "oneOf" => self.one_of.as_ref()?.get(index),
304            "prefixItems" => self.prefix_items.as_ref()?.get(index),
305            _ => None,
306        }
307    }
308
309    /// Look up a child by a JSON pointer segment name.
310    /// This handles both map keywords (where the segment is a key within the map)
311    /// and direct keywords.
312    fn get_map_entry_by_pointer_segment(&self, segment: &str) -> Option<&SchemaValue> {
313        // Try all map-bearing keyword fields.
314        // For pointer navigation, when we're inside a "properties" object,
315        // the segment is the property name.
316        self.properties
317            .get(segment)
318            .or_else(|| self.pattern_properties.get(segment))
319            .or_else(|| self.defs.as_ref().and_then(|m| m.get(segment)))
320            .or_else(|| self.dependent_schemas.get(segment))
321    }
322}
323
324/// Produce a short human-readable type string for a schema.
325fn schema_type_str(schema: &Schema) -> Option<String> {
326    // Explicit type field
327    if let Some(ref ty) = schema.type_ {
328        return match ty {
329            TypeValue::Single(s) if *s == SimpleType::Array => {
330                let item_ty = schema
331                    .items
332                    .as_ref()
333                    .and_then(|sv| sv.as_schema())
334                    .and_then(schema_type_str);
335                match item_ty {
336                    Some(item_ty) => Some(format!("{item_ty}[]")),
337                    None => Some("array".to_string()),
338                }
339            }
340            TypeValue::Single(s) => Some(s.to_string()),
341            TypeValue::Union(arr) => Some(
342                arr.iter()
343                    .map(SimpleType::to_string)
344                    .collect::<Vec<_>>()
345                    .join(" | "),
346            ),
347        };
348    }
349
350    // $ref
351    if let Some(ref r) = schema.ref_ {
352        return Some(ref_name(r).to_string());
353    }
354
355    // oneOf/anyOf
356    for variants in [&schema.one_of, &schema.any_of].into_iter().flatten() {
357        let mut types: Vec<String> = variants
358            .iter()
359            .filter_map(|v| match v {
360                SchemaValue::Schema(s) => {
361                    schema_type_str(s).or_else(|| s.ref_.as_ref().map(|r| ref_name(r).to_string()))
362                }
363                SchemaValue::Bool(_) | SchemaValue::Other(_) => None,
364            })
365            .collect();
366        types.dedup();
367        if !types.is_empty() {
368            return Some(types.join(" | "));
369        }
370    }
371
372    // const
373    if let Some(ref c) = schema.const_ {
374        return Some(format!("const: {c}"));
375    }
376
377    // enum — single-value enums show the value (e.g. `"lf"`), multi-value show `enum`
378    if let Some(ref values) = schema.enum_ {
379        if values.len() == 1 {
380            let val = &values[0];
381            return Some(
382                val.as_str()
383                    .map_or_else(|| val.to_string(), |s| format!("\"{s}\"")),
384            );
385        }
386        return Some("enum".to_string());
387    }
388
389    None
390}