Skip to main content

json_schema_diff/
types.rs

1use schemars::schema::InstanceType;
2use serde::Serialize;
3use thiserror::Error;
4
5/// An "atomic" change made to the JSON schema in question, going from LHS to RHS.
6///
7/// Just a wrapper container for `ChangeKind`
8#[derive(Debug, PartialEq, Serialize)]
9pub struct Change {
10    /// JSON path for the given change. `""` for "root schema". `".foo"` for property foo.
11    pub path: String,
12    /// Data specific to the kind of change.
13    pub change: ChangeKind,
14}
15
16/// The kind of change + data relevant to the change.
17#[derive(Debug, PartialEq, Serialize)]
18pub enum ChangeKind {
19    /// A type has been added and is now additionally allowed.
20    TypeAdd {
21        /// The type in question.
22        added: JsonSchemaType,
23    },
24    /// A type has been removed and is no longer allowed.
25    TypeRemove {
26        /// The type in question.
27        removed: JsonSchemaType,
28    },
29    /// A const value has been added as an allowed value.
30    ConstAdd {
31        /// The value of the added const.
32        added: serde_json::Value,
33    },
34    /// A const has been removed as an allowed value.
35    ConstRemove {
36        /// The value of the removed const.
37        removed: serde_json::Value,
38    },
39    /// A property has been added and (depending on additionalProperties) is now additionally
40    /// allowed.
41    PropertyAdd {
42        /// The value of additionalProperties within the current JSON object.
43        lhs_additional_properties: bool,
44        /// The name of the added property.
45        added: String,
46    },
47    /// A property has been removed and (depending on additionalProperties) might now no longer be
48    /// allowed.
49    PropertyRemove {
50        /// The value of additionalProperties within the current JSON object.
51        lhs_additional_properties: bool,
52        /// The name of the added property.
53        removed: String,
54    },
55    /// A minimum/maximum constraint has been added.
56    RangeAdd {
57        /// The value of the added constraint.
58        added: Range,
59    },
60    /// A minimum/maximum constraint has been removed.
61    RangeRemove {
62        /// The value of the removed constraint.
63        removed: Range,
64    },
65    /// A minimum/maximum constraint has been updated.
66    RangeChange {
67        /// The old constraint value.
68        old_value: Range,
69        /// The new constraint value.
70        new_value: Range,
71    },
72    /// An array-type item has been changed from tuple validation to array validation.
73    ///
74    /// See https://json-schema.org/understanding-json-schema/reference/array.html
75    ///
76    /// Changes will still be emitted for inner items.
77    TupleToArray {
78        /// The length of the (old) tuple
79        old_length: usize,
80    },
81    /// An array-type item has been changed from array validation to tuple validation.
82    ///
83    /// See https://json-schema.org/understanding-json-schema/reference/array.html
84    ///
85    /// Changes will still be emitted for inner items.
86    ArrayToTuple {
87        /// The length of the (new) tuple
88        new_length: usize,
89    },
90    /// An array-type item with tuple validation has changed its length ("items" array got longer
91    /// or shorter.
92    ///
93    /// See https://json-schema.org/understanding-json-schema/reference/array.html
94    ///
95    /// Changes will still be emitted for inner items.
96    TupleChange {
97        /// The new length of the tuple
98        new_length: usize,
99    },
100    /// A previously required property has been removed
101    RequiredRemove {
102        /// The property that is no longer required
103        property: String,
104    },
105    /// A previously optional property has been made required
106    RequiredAdd {
107        /// The property that is now required
108        property: String,
109    },
110    /// A format constraint has been added.
111    FormatAdd {
112        /// The format that was added.
113        added: String,
114    },
115    /// A format constraint has been removed.
116    FormatRemove {
117        /// The format that was removed.
118        removed: String,
119    },
120    /// A format constraint has been changed.
121    FormatChange {
122        /// The old format value.
123        old_format: String,
124        /// The new format value.
125        new_format: String,
126    },
127    /// An enum value has been added to the allowed values.
128    EnumAdd {
129        /// The value that was added to the enum.
130        added: serde_json::Value,
131        /// Whether the enum constraint was added (lhs had no enum).
132        /// If true, this is breaking as it adds a new constraint.
133        lhs_has_no_enum: bool,
134    },
135    /// An enum value has been removed from the allowed values.
136    EnumRemove {
137        /// The value that was removed from the enum.
138        removed: serde_json::Value,
139        /// Whether the entire enum constraint was removed (rhs has no enum).
140        /// If true, this is non-breaking as it relaxes the constraint.
141        rhs_has_no_enum: bool,
142    },
143    /// A pattern constraint has been added.
144    PatternAdd {
145        /// The pattern that was added.
146        added: String,
147    },
148    /// A pattern constraint has been removed.
149    PatternRemove {
150        /// The pattern that was removed.
151        removed: String,
152    },
153    /// A pattern constraint has been changed.
154    PatternChange {
155        /// The old pattern value.
156        old_pattern: String,
157        /// The new pattern value.
158        new_pattern: String,
159    },
160    /// A minLength constraint has been added.
161    MinLengthAdd {
162        /// The minLength value that was added.
163        added: u32,
164    },
165    /// A minLength constraint has been removed.
166    MinLengthRemove {
167        /// The minLength value that was removed.
168        removed: u32,
169    },
170    /// A minLength constraint has been changed.
171    MinLengthChange {
172        /// The old minLength value.
173        old_value: u32,
174        /// The new minLength value.
175        new_value: u32,
176    },
177    /// A maxLength constraint has been added.
178    MaxLengthAdd {
179        /// The maxLength value that was added.
180        added: u32,
181    },
182    /// A maxLength constraint has been removed.
183    MaxLengthRemove {
184        /// The maxLength value that was removed.
185        removed: u32,
186    },
187    /// A maxLength constraint has been changed.
188    MaxLengthChange {
189        /// The old maxLength value.
190        old_value: u32,
191        /// The new maxLength value.
192        new_value: u32,
193    },
194}
195
196impl ChangeKind {
197    /// Whether the change is breaking.
198    ///
199    /// What is considered breaking is WIP. Changes are intentionally exposed as-is in public API
200    /// so that the user can develop their own logic as to what they consider breaking.
201    ///
202    /// Currently the rule of thumb is, a change is breaking if it would cause messages that used
203    /// to validate fine under RHS to no longer validate under LHS.
204    pub fn is_breaking(&self) -> bool {
205        match self {
206            Self::TypeAdd { .. } => false,
207            Self::TypeRemove { .. } => true,
208            Self::ConstAdd { .. } => true,
209            Self::ConstRemove { .. } => false,
210            Self::PropertyAdd {
211                lhs_additional_properties,
212                ..
213            } => *lhs_additional_properties,
214            Self::PropertyRemove {
215                lhs_additional_properties,
216                ..
217            } => !*lhs_additional_properties,
218            Self::RangeAdd { .. } => true,
219            Self::RangeRemove { .. } => false,
220            Self::RangeChange {
221                old_value,
222                new_value,
223            } => match (old_value, new_value) {
224                (Range::ExclusiveMinimum(exc), Range::Minimum(min)) if exc >= min => false,
225                (Range::ExclusiveMaximum(exc), Range::Maximum(max)) if exc <= max => false,
226                (Range::Minimum(l), Range::Minimum(r)) if l >= r => false,
227                (Range::ExclusiveMinimum(l), Range::ExclusiveMinimum(r)) if l >= r => false,
228                (Range::Maximum(l), Range::Maximum(r)) if l <= r => false,
229                (Range::ExclusiveMaximum(l), Range::ExclusiveMaximum(r)) if l <= r => false,
230                _ => true,
231            },
232            Self::TupleToArray { .. } => false,
233            Self::ArrayToTuple { .. } => true,
234            Self::TupleChange { .. } => true,
235            Self::RequiredRemove { .. } => false,
236            Self::RequiredAdd { .. } => true,
237            Self::FormatAdd { .. } => true,
238            Self::FormatRemove { .. } => false,
239            Self::FormatChange { .. } => true,
240            // EnumAdd is breaking only if it adds a new enum constraint (lhs had no enum).
241            // Adding values to an existing enum is non-breaking (accepts more data).
242            Self::EnumAdd {
243                lhs_has_no_enum, ..
244            } => *lhs_has_no_enum,
245            // EnumRemove is breaking if removing values from a surviving enum constraint.
246            // Removing the entire enum constraint is non-breaking (accepts more data).
247            Self::EnumRemove {
248                rhs_has_no_enum, ..
249            } => !rhs_has_no_enum,
250            // Pattern changes are conservatively treated as breaking.
251            // Determining if one regex is a subset of another requires complex analysis.
252            Self::PatternAdd { .. } => true,
253            Self::PatternRemove { .. } => false,
254            Self::PatternChange { .. } => true,
255            // MinLength: increasing restricts (breaking), decreasing relaxes (non-breaking)
256            Self::MinLengthAdd { .. } => true,
257            Self::MinLengthRemove { .. } => false,
258            Self::MinLengthChange {
259                old_value,
260                new_value,
261            } => new_value > old_value,
262            // MaxLength: decreasing restricts (breaking), increasing relaxes (non-breaking)
263            Self::MaxLengthAdd { .. } => true,
264            Self::MaxLengthRemove { .. } => false,
265            Self::MaxLengthChange {
266                old_value,
267                new_value,
268            } => new_value < old_value,
269        }
270    }
271}
272
273/// The errors that can happen in this crate.
274#[derive(Error, Debug)]
275pub enum Error {
276    /// Failed to parse the JSON schema.
277    ///
278    /// Any deserialization errors from serde that happen while converting the value into our AST
279    /// end up here.
280    #[error("failed to parse schema")]
281    Serde(#[from] serde_json::Error),
282}
283
284/// All primitive types defined in JSON schema.
285#[derive(Serialize, Clone, Ord, Eq, PartialEq, PartialOrd, Debug)]
286#[allow(missing_docs)]
287pub enum JsonSchemaType {
288    #[serde(rename = "string")]
289    String,
290    #[serde(rename = "number")]
291    Number,
292    #[serde(rename = "integer")]
293    Integer,
294    #[serde(rename = "object")]
295    Object,
296    #[serde(rename = "array")]
297    Array,
298    #[serde(rename = "boolean")]
299    Boolean,
300    #[serde(rename = "null")]
301    Null,
302}
303
304impl From<JsonSchemaType> for InstanceType {
305    fn from(t: JsonSchemaType) -> Self {
306        match t {
307            JsonSchemaType::String => InstanceType::String,
308            JsonSchemaType::Number => InstanceType::Number,
309            JsonSchemaType::Integer => InstanceType::Integer,
310            JsonSchemaType::Object => InstanceType::Object,
311            JsonSchemaType::Array => InstanceType::Array,
312            JsonSchemaType::Boolean => InstanceType::Boolean,
313            JsonSchemaType::Null => InstanceType::Null,
314        }
315    }
316}
317
318impl From<InstanceType> for JsonSchemaType {
319    fn from(t: InstanceType) -> Self {
320        match t {
321            InstanceType::String => JsonSchemaType::String,
322            InstanceType::Number => JsonSchemaType::Number,
323            InstanceType::Integer => JsonSchemaType::Integer,
324            InstanceType::Object => JsonSchemaType::Object,
325            InstanceType::Array => JsonSchemaType::Array,
326            InstanceType::Boolean => JsonSchemaType::Boolean,
327            InstanceType::Null => JsonSchemaType::Null,
328        }
329    }
330}
331
332/// Range constraints in JSON schema.
333#[derive(Serialize, Clone, PartialEq, PartialOrd, Debug)]
334#[serde(rename_all = "camelCase")]
335#[allow(missing_docs)]
336pub enum Range {
337    Minimum(f64),
338    Maximum(f64),
339    ExclusiveMinimum(f64),
340    ExclusiveMaximum(f64),
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346    #[test]
347    fn is_range_change_breaking() {
348        assert!(!ChangeKind::RangeChange {
349            old_value: Range::Minimum(1.0),
350            new_value: Range::Minimum(1.0),
351        }
352        .is_breaking());
353
354        assert!(ChangeKind::RangeChange {
355            old_value: Range::Minimum(1.0),
356            new_value: Range::Minimum(2.0),
357        }
358        .is_breaking());
359
360        assert!(!ChangeKind::RangeChange {
361            old_value: Range::Minimum(2.0),
362            new_value: Range::Minimum(1.0),
363        }
364        .is_breaking());
365
366        assert!(ChangeKind::RangeChange {
367            old_value: Range::Minimum(1.0),
368            new_value: Range::ExclusiveMinimum(1.0),
369        }
370        .is_breaking());
371
372        assert!(ChangeKind::RangeChange {
373            old_value: Range::Minimum(1.0),
374            new_value: Range::ExclusiveMinimum(2.0),
375        }
376        .is_breaking());
377
378        assert!(ChangeKind::RangeChange {
379            old_value: Range::Minimum(2.0),
380            new_value: Range::ExclusiveMinimum(1.0),
381        }
382        .is_breaking());
383
384        assert!(!ChangeKind::RangeChange {
385            old_value: Range::ExclusiveMinimum(1.0),
386            new_value: Range::ExclusiveMinimum(1.0),
387        }
388        .is_breaking());
389
390        assert!(ChangeKind::RangeChange {
391            old_value: Range::ExclusiveMinimum(1.0),
392            new_value: Range::ExclusiveMinimum(2.0),
393        }
394        .is_breaking());
395
396        assert!(!ChangeKind::RangeChange {
397            old_value: Range::ExclusiveMinimum(2.0),
398            new_value: Range::ExclusiveMinimum(1.0),
399        }
400        .is_breaking());
401
402        assert!(!ChangeKind::RangeChange {
403            old_value: Range::Maximum(1.0),
404            new_value: Range::Maximum(1.0),
405        }
406        .is_breaking());
407
408        assert!(!ChangeKind::RangeChange {
409            old_value: Range::Maximum(1.0),
410            new_value: Range::Maximum(2.0),
411        }
412        .is_breaking());
413
414        assert!(ChangeKind::RangeChange {
415            old_value: Range::Maximum(2.0),
416            new_value: Range::Maximum(1.0),
417        }
418        .is_breaking());
419
420        assert!(ChangeKind::RangeChange {
421            old_value: Range::Maximum(1.0),
422            new_value: Range::ExclusiveMaximum(1.0),
423        }
424        .is_breaking());
425
426        assert!(ChangeKind::RangeChange {
427            old_value: Range::Maximum(1.0),
428            new_value: Range::ExclusiveMaximum(2.0),
429        }
430        .is_breaking());
431
432        assert!(ChangeKind::RangeChange {
433            old_value: Range::Maximum(2.0),
434            new_value: Range::ExclusiveMaximum(1.0),
435        }
436        .is_breaking());
437
438        assert!(!ChangeKind::RangeChange {
439            old_value: Range::ExclusiveMaximum(1.0),
440            new_value: Range::ExclusiveMaximum(1.0),
441        }
442        .is_breaking());
443
444        assert!(!ChangeKind::RangeChange {
445            old_value: Range::ExclusiveMaximum(1.0),
446            new_value: Range::ExclusiveMaximum(2.0),
447        }
448        .is_breaking());
449
450        assert!(ChangeKind::RangeChange {
451            old_value: Range::ExclusiveMaximum(2.0),
452            new_value: Range::ExclusiveMaximum(1.0),
453        }
454        .is_breaking());
455    }
456}