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