1use core::fmt::Write;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub struct ValidationError {
9 pub instance_path: String,
11 pub schema_path: String,
13 pub kind: ValidationErrorKind,
15 pub span: (usize, usize),
17}
18
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24#[serde(tag = "type")]
25pub enum ValidationErrorKind {
26 AdditionalItems {
27 limit: usize,
28 },
29 AdditionalProperty {
31 property: String,
32 },
33 AnyOf,
34 BacktrackLimitExceeded {
35 message: String,
36 },
37 Constant {
38 expected_value: Value,
39 },
40 Contains,
41 ContentEncoding {
42 content_encoding: String,
43 },
44 ContentMediaType {
45 content_media_type: String,
46 },
47 Custom {
48 keyword: String,
49 message: String,
50 },
51 Enum {
52 options: Value,
53 },
54 ExclusiveMaximum {
55 limit: Value,
56 },
57 ExclusiveMinimum {
58 limit: Value,
59 },
60 FalseSchema,
61 Format {
62 format: String,
63 },
64 FromUtf8 {
65 message: String,
66 },
67 MaxItems {
68 limit: u64,
69 },
70 Maximum {
71 limit: Value,
72 },
73 MaxLength {
74 limit: u64,
75 },
76 MaxProperties {
77 limit: u64,
78 },
79 MinItems {
80 limit: u64,
81 },
82 Minimum {
83 limit: Value,
84 },
85 MinLength {
86 limit: u64,
87 },
88 MinProperties {
89 limit: u64,
90 },
91 MultipleOf {
92 multiple_of: f64,
93 },
94 Not,
95 OneOfMultipleValid,
96 OneOfNotValid,
97 Pattern {
98 pattern: String,
99 },
100 PropertyNames {
101 message: String,
102 },
103 Required {
104 property: String,
105 },
106 Type {
107 expected: String,
108 },
109 UnevaluatedItems {
110 unexpected: Vec<String>,
111 },
112 UnevaluatedProperties {
113 unexpected: Vec<String>,
114 },
115 UniqueItems,
116 Referencing {
117 message: String,
118 },
119}
120
121impl ValidationErrorKind {
122 #[allow(clippy::match_same_arms)]
124 pub fn message(&self) -> String {
125 match self {
126 Self::AdditionalItems { limit } => {
127 format!("Additional items are not allowed (limit: {limit})")
128 }
129 Self::AdditionalProperty { property } => {
130 format!("Additional properties are not allowed ('{property}' was unexpected)")
131 }
132 Self::AnyOf => {
133 "not valid under any of the schemas listed in the 'anyOf' keyword".to_string()
134 }
135 Self::BacktrackLimitExceeded { message }
136 | Self::Custom { message, .. }
137 | Self::FromUtf8 { message }
138 | Self::PropertyNames { message }
139 | Self::Referencing { message } => message.clone(),
140 Self::Constant { expected_value } => format!("{expected_value} was expected"),
141 Self::Contains => "None of the items are valid under the given schema".to_string(),
142 Self::ContentEncoding { content_encoding } => {
143 format!(r#"not compliant with "{content_encoding}" content encoding"#)
144 }
145 Self::ContentMediaType { content_media_type } => {
146 format!(r#"not compliant with "{content_media_type}" media type"#)
147 }
148 Self::Enum { options } => {
149 let mut msg = String::new();
150 if let Value::Array(arr) = options {
151 let _ = write!(msg, "value is not one of: ");
152 for (i, opt) in arr.iter().enumerate() {
153 if i > 0 {
154 let _ = write!(msg, ", ");
155 }
156 let _ = write!(msg, "{opt}");
157 }
158 } else {
159 let _ = write!(msg, "{options} was expected");
160 }
161 msg
162 }
163 Self::ExclusiveMaximum { limit } => {
164 format!("value is greater than or equal to the maximum of {limit}")
165 }
166 Self::ExclusiveMinimum { limit } => {
167 format!("value is less than or equal to the minimum of {limit}")
168 }
169 Self::FalseSchema => "False schema does not allow any value".to_string(),
170 Self::Format { format } => format!(r#"value is not a "{format}""#),
171 Self::MaxItems { limit } => {
172 let s = if *limit == 1 { "" } else { "s" };
173 format!("array has more than {limit} item{s}")
174 }
175 Self::Maximum { limit } => format!("value is greater than the maximum of {limit}"),
176 Self::MaxLength { limit } => {
177 let s = if *limit == 1 { "" } else { "s" };
178 format!("string is longer than {limit} character{s}")
179 }
180 Self::MaxProperties { limit } => {
181 let s = if *limit == 1 { "y" } else { "ies" };
182 format!("object has more than {limit} propert{s}")
183 }
184 Self::MinItems { limit } => {
185 let s = if *limit == 1 { "" } else { "s" };
186 format!("array has less than {limit} item{s}")
187 }
188 Self::Minimum { limit } => format!("value is less than the minimum of {limit}"),
189 Self::MinLength { limit } => {
190 let s = if *limit == 1 { "" } else { "s" };
191 format!("string is shorter than {limit} character{s}")
192 }
193 Self::MinProperties { limit } => {
194 let s = if *limit == 1 { "y" } else { "ies" };
195 format!("object has less than {limit} propert{s}")
196 }
197 Self::MultipleOf { multiple_of } => {
198 format!("value is not a multiple of {multiple_of}")
199 }
200 Self::Not => "value should not be valid under the given schema".to_string(),
201 Self::OneOfMultipleValid => {
202 "valid under more than one of the schemas listed in the 'oneOf' keyword".to_string()
203 }
204 Self::OneOfNotValid => {
205 "not valid under any of the schemas listed in the 'oneOf' keyword".to_string()
206 }
207 Self::Pattern { pattern } => format!(r#"value does not match "{pattern}""#),
208 Self::Required { property } => format!("{property} is a required property"),
209 Self::Type { expected } => format!(r#"value is not of type "{expected}""#),
210 Self::UnevaluatedItems { unexpected } => {
211 let mut msg = "Unevaluated items are not allowed (".to_string();
212 write_quoted_list(&mut msg, unexpected);
213 write_unexpected_suffix(&mut msg, unexpected.len());
214 msg
215 }
216 Self::UnevaluatedProperties { unexpected } => {
217 let mut msg = "Unevaluated properties are not allowed (".to_string();
218 write_quoted_list(&mut msg, unexpected);
219 write_unexpected_suffix(&mut msg, unexpected.len());
220 msg
221 }
222 Self::UniqueItems => "array has non-unique elements".to_string(),
223 }
224 }
225}
226
227fn write_quoted_list(buf: &mut String, items: &[String]) {
228 for (i, item) in items.iter().enumerate() {
229 if i > 0 {
230 let _ = write!(buf, ", ");
231 }
232 let _ = write!(buf, "'{item}'");
233 }
234}
235
236fn write_unexpected_suffix(buf: &mut String, count: usize) {
237 if count == 1 {
238 let _ = write!(buf, " was unexpected)");
239 } else {
240 let _ = write!(buf, " were unexpected)");
241 }
242}
243
244#[cfg(test)]
245#[allow(clippy::unwrap_used)]
246mod tests {
247 use super::*;
248 use serde_json::json;
249
250 #[test]
251 fn additional_property_message() {
252 let kind = ValidationErrorKind::AdditionalProperty {
253 property: "foo".to_string(),
254 };
255 assert_eq!(
256 kind.message(),
257 "Additional properties are not allowed ('foo' was unexpected)"
258 );
259 }
260
261 #[test]
262 fn required_message() {
263 let kind = ValidationErrorKind::Required {
264 property: "\"name\"".to_string(),
265 };
266 assert_eq!(kind.message(), "\"name\" is a required property");
267 }
268
269 #[test]
270 fn type_message() {
271 let kind = ValidationErrorKind::Type {
272 expected: "string".to_string(),
273 };
274 assert_eq!(kind.message(), r#"value is not of type "string""#);
275 }
276
277 #[test]
278 fn enum_message() {
279 let kind = ValidationErrorKind::Enum {
280 options: json!(["a", "b", "c"]),
281 };
282 assert_eq!(kind.message(), r#"value is not one of: "a", "b", "c""#);
283 }
284
285 #[test]
286 fn serialization_roundtrip() {
287 let error = ValidationError {
288 instance_path: "/name".to_string(),
289 schema_path: "/properties/name/type".to_string(),
290 kind: ValidationErrorKind::Type {
291 expected: "string".to_string(),
292 },
293 span: (10, 5),
294 };
295 let json = serde_json::to_string(&error).unwrap();
296 let deserialized: ValidationError = serde_json::from_str(&json).unwrap();
297 assert_eq!(error, deserialized);
298 }
299
300 #[test]
301 fn additional_property_serialization() {
302 let error = ValidationError {
303 instance_path: "/foo".to_string(),
304 schema_path: "/additionalProperties".to_string(),
305 kind: ValidationErrorKind::AdditionalProperty {
306 property: "foo".to_string(),
307 },
308 span: (5, 3),
309 };
310 let json = serde_json::to_string(&error).unwrap();
311 let deserialized: ValidationError = serde_json::from_str(&json).unwrap();
312 assert_eq!(error, deserialized);
313 }
314}