Skip to main content

json_schema_rs/validator/
error.rs

1use crate::json_pointer::JsonPointer;
2use std::fmt;
3
4/// Wraps f64 so that `ValidationError` can derive Eq (f64 is not Eq; comparison is by bits).
5#[derive(Debug, Clone, Copy)]
6pub struct OrderedF64(pub f64);
7
8impl PartialEq for OrderedF64 {
9    fn eq(&self, other: &Self) -> bool {
10        self.0.to_bits() == other.0.to_bits()
11    }
12}
13
14impl Eq for OrderedF64 {}
15
16pub type ValidationResult = Result<(), Vec<ValidationError>>;
17
18/// A single validation failure: kind and instance location.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum ValidationError {
21    /// Schema had an invalid or unsupported `$ref`, or the reference could not be resolved.
22    InvalidRef {
23        /// JSON Pointer to the instance location where the referenced schema was applied.
24        instance_path: JsonPointer,
25        /// The `$ref` string from the schema.
26        ref_str: String,
27        /// Human-readable reason (for user-facing context).
28        reason: String,
29    },
30    /// Schema had `type: "object"` but the instance was not an object.
31    ExpectedObject {
32        /// JSON Pointer to the instance that failed.
33        instance_path: JsonPointer,
34        /// JSON type of the instance (for user-facing context).
35        got: String,
36    },
37    /// Schema had `type: "string"` but the instance was not a string.
38    ExpectedString {
39        /// JSON Pointer to the instance that failed.
40        instance_path: JsonPointer,
41        /// JSON type of the instance (for user-facing context).
42        got: String,
43    },
44    /// Schema had `type: "integer"` but the instance was not an integer (e.g. float, string, or non-number).
45    ExpectedInteger {
46        /// JSON Pointer to the instance that failed.
47        instance_path: JsonPointer,
48        /// JSON type of the instance (for user-facing context).
49        got: String,
50    },
51    /// Schema had `type: "number"` but the instance was not a number (e.g. string, null, or non-number).
52    ExpectedNumber {
53        /// JSON Pointer to the instance that failed.
54        instance_path: JsonPointer,
55        /// JSON type of the instance (for user-facing context).
56        got: String,
57    },
58    /// Schema had `type: "array"` but the instance was not an array.
59    ExpectedArray {
60        /// JSON Pointer to the instance that failed.
61        instance_path: JsonPointer,
62        /// JSON type of the instance (for user-facing context).
63        got: String,
64    },
65    /// Schema had `type: "boolean"` but the instance was not a boolean.
66    ExpectedBoolean {
67        /// JSON Pointer to the instance that failed.
68        instance_path: JsonPointer,
69        /// JSON type of the instance (for user-facing context).
70        got: String,
71    },
72    /// Schema had `uniqueItems: true` but the array contained duplicate elements.
73    DuplicateArrayItems {
74        /// JSON Pointer to the array instance that failed.
75        instance_path: JsonPointer,
76        /// Serialized duplicate value (for user-facing context).
77        duplicate_value: String,
78    },
79    /// Schema had `minItems` but the array had fewer elements.
80    TooFewItems {
81        /// JSON Pointer to the array instance that failed.
82        instance_path: JsonPointer,
83        /// The schema's minItems value.
84        min_items: u64,
85        /// Actual number of items in the array (for user-facing context).
86        actual_count: u64,
87    },
88    /// Schema had `maxItems` but the array had more elements.
89    TooManyItems {
90        /// JSON Pointer to the array instance that failed.
91        instance_path: JsonPointer,
92        /// The schema's maxItems value.
93        max_items: u64,
94        /// Actual number of items in the array (for user-facing context).
95        actual_count: u64,
96    },
97    /// A property listed in `required` was absent.
98    MissingRequired {
99        /// JSON Pointer to the object (parent of the missing property).
100        instance_path: JsonPointer,
101        /// The required property name that was missing.
102        property: String,
103    },
104    /// Schema had `additionalProperties: false` but the instance contained a property not in `properties`.
105    DisallowedAdditionalProperty {
106        /// JSON Pointer to the instance (the additional property).
107        instance_path: JsonPointer,
108        /// The property name that is not allowed.
109        property: String,
110    },
111    /// Schema had `enum` but the instance value was not one of the allowed values.
112    NotInEnum {
113        /// JSON Pointer to the instance that failed.
114        instance_path: JsonPointer,
115        /// Serialized invalid value (for user-facing context).
116        invalid_value: String,
117        /// Serialized allowed enum values (for user-facing context).
118        allowed: Vec<String>,
119    },
120    /// Schema had `const` but the instance value was not equal to the const value.
121    NotConst {
122        /// JSON Pointer to the instance that failed.
123        instance_path: JsonPointer,
124        /// Serialized expected (const) value (for user-facing context).
125        expected: String,
126        /// Serialized actual instance value (for user-facing context).
127        actual: String,
128    },
129    /// Instance was below the schema's `minimum` (inclusive lower bound).
130    BelowMinimum {
131        /// JSON Pointer to the instance that failed.
132        instance_path: JsonPointer,
133        /// The schema's minimum value.
134        minimum: OrderedF64,
135        /// Actual instance value (for user-facing context).
136        actual: OrderedF64,
137    },
138    /// Instance was above the schema's `maximum` (inclusive upper bound).
139    AboveMaximum {
140        /// JSON Pointer to the instance that failed.
141        instance_path: JsonPointer,
142        /// The schema's maximum value.
143        maximum: OrderedF64,
144        /// Actual instance value (for user-facing context).
145        actual: OrderedF64,
146    },
147    /// Schema had `minLength` but the string had fewer Unicode code points.
148    TooShort {
149        /// JSON Pointer to the instance that failed.
150        instance_path: JsonPointer,
151        /// The schema's minLength value.
152        min_length: u64,
153        /// Actual Unicode code point count (for user-facing context).
154        actual_length: u64,
155    },
156    /// Schema had `maxLength` but the string had more Unicode code points.
157    TooLong {
158        /// JSON Pointer to the instance that failed.
159        instance_path: JsonPointer,
160        /// The schema's maxLength value.
161        max_length: u64,
162        /// Actual Unicode code point count (for user-facing context).
163        actual_length: u64,
164    },
165    /// Schema had `pattern` but the string did not match the ECMA 262 regex.
166    PatternMismatch {
167        /// JSON Pointer to the instance that failed.
168        instance_path: JsonPointer,
169        /// The schema's pattern value (ECMA 262 regex string).
170        pattern: String,
171        /// The instance string value (for user-facing context).
172        value: String,
173    },
174    /// Schema had an invalid `pattern` (not a valid ECMA 262 regex); compilation failed.
175    InvalidPatternInSchema {
176        /// JSON Pointer to the instance (schema location where pattern was applied).
177        instance_path: JsonPointer,
178        /// The invalid pattern string from the schema.
179        pattern: String,
180    },
181    /// The string instance does not parse as a valid UUID (only emitted when the `uuid` feature is enabled).
182    #[cfg(feature = "uuid")]
183    InvalidUuidFormat {
184        /// JSON Pointer to the instance that failed.
185        instance_path: JsonPointer,
186        /// The invalid string value (for user-facing context).
187        value: String,
188    },
189    /// Schema had `anyOf` but the instance did not validate against any of the subschemas.
190    NoSubschemaMatched {
191        /// JSON Pointer to the instance that failed.
192        instance_path: JsonPointer,
193        /// Number of subschemas in the anyOf (for user-facing context).
194        subschema_count: usize,
195    },
196    /// Schema had `oneOf` but the instance validated against more than one subschema.
197    MultipleSubschemasMatched {
198        /// JSON Pointer to the instance that failed.
199        instance_path: JsonPointer,
200        /// Number of subschemas in the oneOf (for user-facing context).
201        subschema_count: usize,
202        /// Number of subschemas that passed validation (must be >= 2 for this error).
203        match_count: usize,
204    },
205}
206
207impl std::error::Error for ValidationError {}
208
209impl ValidationError {
210    #[must_use]
211    pub fn instance_path(&self) -> &JsonPointer {
212        match self {
213            ValidationError::InvalidRef { instance_path, .. }
214            | ValidationError::ExpectedObject { instance_path, .. }
215            | ValidationError::ExpectedString { instance_path, .. }
216            | ValidationError::ExpectedInteger { instance_path, .. }
217            | ValidationError::ExpectedNumber { instance_path, .. }
218            | ValidationError::ExpectedArray { instance_path, .. }
219            | ValidationError::ExpectedBoolean { instance_path, .. }
220            | ValidationError::DuplicateArrayItems { instance_path, .. }
221            | ValidationError::TooFewItems { instance_path, .. }
222            | ValidationError::TooManyItems { instance_path, .. }
223            | ValidationError::MissingRequired { instance_path, .. }
224            | ValidationError::DisallowedAdditionalProperty { instance_path, .. }
225            | ValidationError::NotInEnum { instance_path, .. }
226            | ValidationError::NotConst { instance_path, .. }
227            | ValidationError::BelowMinimum { instance_path, .. }
228            | ValidationError::AboveMaximum { instance_path, .. }
229            | ValidationError::TooShort { instance_path, .. }
230            | ValidationError::TooLong { instance_path, .. }
231            | ValidationError::PatternMismatch { instance_path, .. }
232            | ValidationError::InvalidPatternInSchema { instance_path, .. }
233            | ValidationError::NoSubschemaMatched { instance_path, .. }
234            | ValidationError::MultipleSubschemasMatched { instance_path, .. } => instance_path,
235            #[cfg(feature = "uuid")]
236            ValidationError::InvalidUuidFormat { instance_path, .. } => instance_path,
237        }
238    }
239}
240
241impl fmt::Display for ValidationError {
242    #[expect(clippy::too_many_lines)]
243    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244        let location = self.instance_path().display_root_or_path();
245        match self {
246            ValidationError::InvalidRef {
247                ref_str, reason, ..
248            } => {
249                write!(
250                    f,
251                    "{location}: could not resolve $ref \"{ref_str}\": {reason}"
252                )
253            }
254            ValidationError::ExpectedObject { got, .. } => {
255                write!(f, "{location}: expected object, got {got}")
256            }
257            ValidationError::ExpectedString { got, .. } => {
258                write!(f, "{location}: expected string, got {got}")
259            }
260            ValidationError::ExpectedInteger { got, .. } => {
261                write!(f, "{location}: expected integer, got {got}")
262            }
263            ValidationError::ExpectedNumber { got, .. } => {
264                write!(f, "{location}: expected number, got {got}")
265            }
266            ValidationError::ExpectedArray { got, .. } => {
267                write!(f, "{location}: expected array, got {got}")
268            }
269            ValidationError::ExpectedBoolean { got, .. } => {
270                write!(f, "{location}: expected boolean, got {got}")
271            }
272            ValidationError::DuplicateArrayItems {
273                duplicate_value, ..
274            } => {
275                write!(
276                    f,
277                    "{location}: array has duplicate items (value: {duplicate_value})"
278                )
279            }
280            ValidationError::TooFewItems {
281                min_items,
282                actual_count,
283                ..
284            } => {
285                write!(
286                    f,
287                    "{location}: array has {actual_count} item(s), minimum is {min_items}"
288                )
289            }
290            ValidationError::TooManyItems {
291                max_items,
292                actual_count,
293                ..
294            } => {
295                write!(
296                    f,
297                    "{location}: array has {actual_count} item(s), maximum is {max_items}"
298                )
299            }
300            ValidationError::MissingRequired { property, .. } => {
301                write!(f, "{location}: missing required property \"{property}\"")
302            }
303            ValidationError::DisallowedAdditionalProperty { property, .. } => {
304                write!(
305                    f,
306                    "{location}: additional property \"{property}\" not allowed"
307                )
308            }
309            ValidationError::NotInEnum {
310                invalid_value,
311                allowed,
312                ..
313            } => {
314                let allowed_str: String = allowed.join(", ");
315                write!(
316                    f,
317                    "{location}: value {invalid_value} not in enum (allowed: {allowed_str})"
318                )
319            }
320            ValidationError::NotConst {
321                expected, actual, ..
322            } => {
323                write!(
324                    f,
325                    "{location}: value {actual} does not match const (expected: {expected})"
326                )
327            }
328            ValidationError::BelowMinimum {
329                minimum, actual, ..
330            } => {
331                write!(
332                    f,
333                    "{location}: value {} is below minimum {}",
334                    actual.0, minimum.0
335                )
336            }
337            ValidationError::AboveMaximum {
338                maximum, actual, ..
339            } => {
340                write!(
341                    f,
342                    "{location}: value {} is above maximum {}",
343                    actual.0, maximum.0
344                )
345            }
346            ValidationError::TooShort {
347                min_length,
348                actual_length,
349                ..
350            } => {
351                write!(
352                    f,
353                    "{location}: string has {actual_length} code points, minLength is {min_length}"
354                )
355            }
356            ValidationError::TooLong {
357                max_length,
358                actual_length,
359                ..
360            } => {
361                write!(
362                    f,
363                    "{location}: string has {actual_length} code points, maxLength is {max_length}"
364                )
365            }
366            ValidationError::PatternMismatch { pattern, value, .. } => {
367                write!(
368                    f,
369                    "{location}: string \"{value}\" does not match pattern \"{pattern}\""
370                )
371            }
372            ValidationError::InvalidPatternInSchema { pattern, .. } => {
373                write!(f, "{location}: schema has invalid pattern \"{pattern}\"")
374            }
375            #[cfg(feature = "uuid")]
376            ValidationError::InvalidUuidFormat { value, .. } => {
377                write!(f, "{location}: string \"{value}\" is not a valid UUID")
378            }
379            ValidationError::NoSubschemaMatched {
380                subschema_count, ..
381            } => {
382                write!(
383                    f,
384                    "{location}: instance does not match any of the {subschema_count} subschema(s)"
385                )
386            }
387            ValidationError::MultipleSubschemasMatched {
388                subschema_count,
389                match_count,
390                ..
391            } => {
392                write!(
393                    f,
394                    "{location}: instance matches {match_count} of the {subschema_count} oneOf subschema(s), exactly one required"
395                )
396            }
397        }
398    }
399}