OpenAPI

Schema diff

Detect breaking changes between two JSON Schemas with vld::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.

Cargo.toml
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

ItemPurpose
diff_schemas(&old, &new) -> SchemaDiffcompare
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::NonBreakingseverity

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.

BreakingNon-breaking
new required fieldnew optional field
removed fieldfield became optional
type changedconstraint relaxed (maxLength raised, minimum lowered)
constraint tightened, new format / patternformat / pattern removed
null no longer acceptednull now accepted
enum value removedenum 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());
}