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}