Schema diff
The diff feature (it enables openapi) compares two JSON Schemas and classifies every change as breaking or non-breaking. Use it in CI to catch API changes that would break existing clients.
vld = { version = "0.5", features = ["diff"] }
use serde_json::json;
use vld::diff::{diff_schemas, ChangeKind};
let old = json!({
"type": "object",
"required": ["name", "email"],
"properties": {
"name": { "type": "string" },
"email": { "type": "string", "format": "email" }
}
});
let new = json!({
"type": "object",
"required": ["name", "email", "age"],
"properties": {
"name": { "type": "string" },
"email": { "type": "string" },
"age": { "type": "integer" }
}
});
let diff = diff_schemas(&old, &new);
assert!(diff.has_breaking()); // "age" is a new required field
for change in &diff.changes {
let tag = if change.kind == ChangeKind::Breaking { "BREAKING" } else { "ok" };
println!("[{tag}] {}: {}", change.path, change.description);
}
diff_schemas works on plain serde_json::Values, so the schemas can come from json_schema(), a stored snapshot or another tool.
API
| Item | Purpose |
|---|---|
diff_schemas(&old, &new) -> SchemaDiff | compare |
SchemaDiff { changes } | every change |
has_breaking() / breaking_changes() / non_breaking_changes() | filter |
SchemaChange { path, kind, description } | one change; path is dot-separated (properties.email.format) |
ChangeKind::Breaking / ChangeKind::NonBreaking | severity |
Direction
The classification assumes request (input) schemas: a change is breaking when the new schema rejects input the old one accepted, and non-breaking when it only accepts more. For response schemas read it the other way round.
| Breaking | Non-breaking |
|---|---|
| new required field | new optional field |
| removed field | field became optional |
| type changed | constraint relaxed (maxLength raised, minimum lowered) |
constraint tightened, new format / pattern | format / pattern removed |
null no longer accepted | null now accepted |
| enum value removed | enum value added |
What is compared
type, required, properties, format, minimum / maximum (and exclusive forms), minLength / maxLength, minItems / maxItems, minProperties / maxProperties, minContains / maxContains, multipleOf, const, enum, pattern, uniqueItems, items, prefixItems, contains, propertyNames, additionalProperties, formatMinimum / formatMaximum, anyOf / oneOf / allOf (position by position), $ref (by name), and vld's x-min* / x-max* / x-exclusive* bounds and x-past / x-future flags.
Nullable wrappers (anyOf of [X, {"type": "null"}]) are unwrapped, so changes inside optional fields are found.
Snapshot workflow
#[test]
fn api_has_no_breaking_changes() {
let old: serde_json::Value =
serde_json::from_str(include_str!("snapshots/create_user.json")).unwrap();
let new = CreateUser::json_schema();
let diff = vld::diff::diff_schemas(&old, &new);
assert!(!diff.has_breaking(), "{:#?}", diff.breaking_changes());
}
JSON Schema and OpenAPI
Generate JSON Schema and OpenAPI 3.1 documents from vld schemas, including nested components and vld's x-* extensions.
Web adapters overview
What every vld web integration has in common — extractors, status codes, the error body, coercion, header and cookie rules, and body limits.