Validation layers¶
pacto validate tells you whether a contract is well-formed before anyone
relies on it. Run it in the bundle directory:
It runs three checks in order and then the
environments stage, and so does anything else that has
to trust a contract. pack, push and publish refuse before they write or
transmit a byte, and the operator revalidates every bundle it loads.
Each layer short-circuits: if it fails, the layers after it are skipped, so one
broken field can hide the rest.
Layer 1: Structural (JSON Schema)¶
Validates against the embedded schema of the contract's version, pacto-v2.0.schema.json, pacto-v2.1.schema.json, pacto-v2.2.schema.json or pacto-v2.3.schema.json:
- Field types match
- Required fields are present (including the mandatory
requiredonconfigurations[]anddependencies[]) - Enum values are valid (for example interface
typeisopenapi/asyncapi/grpc/mcp,capabilities[].typeishealth/metrics/extension,policies[].targetiscontract) - Fields that must carry a value are non-empty (for example
interfaces[].refandchecks[].file) - Discriminated shapes hold: a
configurations[]entry has exactly one ofschemaorref, acapabilities[]extensionrequires a namespacedrefandhealthormetricssets noref - State invariants hold (
statelessrequiresephemeraldurability)
Structural failures report SCHEMA_VIOLATION. A "2.0" contract that uses
publish, checks, an external dependency or an mcp interface reports NEEDS_PACTO_2_1 for each
instead: set pactoVersion: "2.1", which keeps every 2.0 field. A "2.0" or
"2.1" contract that uses an interface lifecycle, compatibilityEpoch or
guarantees or a dependency requires reports NEEDS_PACTO_2_2 for each: set
pactoVersion: "2.2". A "2.0", "2.1" or "2.2" contract that declares
environments reports NEEDS_PACTO_2_3: set pactoVersion: "2.3".
A contract that fails to load, because of malformed YAML, an unknown field or a
pactoVersion other than "2.0", "2.1", "2.2" or "2.3", fails before any layer runs and reports
PARSE_ERROR. Loading is the parse step that turns pacto.yaml into a
contract. Validation starts once it succeeds.
Layer 2: Cross-field¶
Validates semantic references and consistency:
| Rule | Code |
|---|---|
service.version parses as semver, leniently: 1, 0.1 and v0.1 all pass |
INVALID_SEMVER |
| Interface names are unique | DUPLICATE_INTERFACE_NAME |
interfaces[].lifecycle dates are strict YYYY-MM-DD and sunset is not before deprecated |
INVALID_INTERFACE_LIFECYCLE |
interfaces[].lifecycle.successor names another interface or is an absolute URL |
INTERFACE_SUCCESSOR_UNKNOWN |
| Configuration names are unique | DUPLICATE_CONFIGURATION_NAME |
| Policy names are unique | DUPLICATE_POLICY_NAME |
| Dependency names are unique | DUPLICATE_DEPENDENCY_NAME |
| Interface spec file exists in the bundle | FILE_NOT_FOUND |
Interface spec file parses, if the ref ends .json, .yaml or .yml (for any other extension, only existence is checked) |
INVALID_INTERFACE_SPEC |
An mcp interface file lists tools, each with a name used once |
INVALID_MCP_TOOLS |
| Check names are unique | DUPLICATE_CHECK_NAME |
checks[].file exists in the bundle |
FILE_NOT_FOUND |
extension capability ref is well-formed |
CAPABILITY_REF_INVALID |
| Each standard capability type is declared once | DUPLICATE_CAPABILITY |
capabilities[].binding.interface matches a declared interface |
CAPABILITY_INTERFACE_UNKNOWN |
capabilities[].binding.path is a safe application path |
CAPABILITY_PATH_INVALID |
OCI dependency refs (oci://) are valid OCI references |
INVALID_OCI_REF |
A dependency without ref is external |
DEPENDENCY_REF_REQUIRED |
An external dependency without ref has a version |
EXTERNAL_VERSION_REQUIRED |
compatibility appears only with ref, and version only without it |
COMPATIBILITY_WITHOUT_REF, VERSION_WITH_REF |
A dependency with ref has a compatibility range |
EMPTY_COMPATIBILITY |
| Compatibility ranges are valid semver constraints | INVALID_COMPATIBILITY |
| OCI dependency pins a digest | TAG_NOT_DIGEST (warning) |
configurations[].ref is a valid OCI reference |
INVALID_CONFIG_REF |
configurations[].schema file exists in the bundle |
FILE_NOT_FOUND |
configurations[].schema file is valid JSON |
INVALID_CONFIG_JSON |
configurations[].schema file is valid JSON Schema |
INVALID_CONFIG_SCHEMA |
configurations[].values match the schema |
CONFIG_VALUES_VALIDATION_FAILED |
policies[].schema file exists in the bundle |
FILE_NOT_FOUND |
policies[].schema file is valid JSON |
INVALID_POLICY_JSON |
policies[].schema file is valid JSON Schema |
INVALID_POLICY_SCHEMA |
policies[].ref is a valid OCI reference |
INVALID_POLICY_REF |
| Environment names are unique | DUPLICATE_ENVIRONMENT_NAME |
environments[].file is a path in the bundle and the file exists |
FILE_NOT_FOUND |
readiness.claims[].id are unique within the contract |
DUPLICATE_READINESS_ID |
readiness.claims[].evidence has non-whitespace content |
EMPTY_READINESS_EVIDENCE |
readiness.claims[].description (when present) has non-whitespace content |
EMPTY_READINESS_DESCRIPTION |
readiness.expires is a strict YYYY-MM-DD date |
INVALID_READINESS_EXPIRES |
readiness.history[].{date,version,author,description} are valid/non-blank |
INVALID_READINESS_REVISION |
This table lists the codes you can see. Layer 1 rejects the contract first and
reports SCHEMA_VIOLATION wherever the JSON Schema already constrains a field. That covers the interface and capability type enums, a non-empty
interfaces[].ref or dependencies[].compatibility, the configurations[]
schema/ref choice, policies[].target and the stateless/ephemeral
invariant. Layer 2 has
its own check for each of those, but a contract that breaks them never gets past
Layer 1. Build CI rules only on codes in this table.
Layer 3: Policy checks¶
Resolves every declared policy and checks the contract against it. Policies apply with strict AND semantics: the contract must satisfy all of them. Policies have no precedence or override logic, so contradictory policies fail.
| Condition | Code |
|---|---|
policies[].ref cannot be resolved (resolver unavailable, network error, bundle not found) |
POLICY_REF_UNRESOLVED |
| Cycle detected in recursive policy resolution chain | POLICY_REF_CYCLE |
| Contract violates a policy constraint | POLICY_VIOLATION |
policies[].ref was skipped because no resolver was configured |
POLICY_REF_NOT_ENFORCED (warning) |
POLICY_REF_UNRESOLVED is a hard error. Validation fails closed when a referenced policy cannot be resolved.
When ref-based policies are resolved. Recursive resolution of
policies[].ref happens only when a resolver is configured. pacto validate and
pacto push both supply one, since they can reach OCI/file refs. A ref policy
that cannot be fetched then fails closed with POLICY_REF_UNRESOLVED, which is
how pacto push checks remote policies before publishing. pacto pack and the operator pass no resolver. They check only the
locally compiled schema-based policies and skip ref policies, so they never
report the unresolved error. Cycle detection is per chain: a ref is a
cycle only when it reappears in its own resolution chain (A → B → A); two
sibling policies pointing at the same ref resolve independently.
Dependency closure¶
By default Layer 3 checks the root contract only. pacto validate --closure
also checks every contract in the dependency closure against the policies the
root resolves. Each dependency's own policies[] is left to its owner's
validation. The closure resolves the same way pacto graph does, with one fetch
per dependency. External dependencies carry no contract and are skipped.
| Condition | Code |
|---|---|
| A dependency violates a policy the root resolves | CLOSURE_POLICY_VIOLATION |
| A dependency cannot be resolved, sits in a dependency cycle or has an unreadable contract | CLOSURE_DEPENDENCY_UNRESOLVED |
| A direct dependency declares a deprecated interface | DEPENDENCY_INTERFACE_DEPRECATED (warning) |
| That interface has reached its sunset | DEPENDENCY_INTERFACE_SUNSET (warning) |
A policy finding names the dependency in its path, for example
dependencies[ledger@1.0.0]. A dependency that cannot be resolved is named in
the message. When a dependency declared it, the path names that dependency. An
unresolvable dependency fails the run, so the check fails closed.
A deprecated interface is a warning that names the interface, its sunset and its
successor, with the path of the dependency that resolved to it, for example
dependencies[1]. Both codes stay warnings so the verdict does not change with
the date the check runs. To fail a pipeline after the sunset, gate on
DEPENDENCY_INTERFACE_SUNSET in --output-format json. When pacto.lock is present, the lock is verified first and
a drifted closure fails with the lock error before the closure is checked.
Environments stage¶
After the policy layer, the environments stage checks each environment's
file against the schema of every configuration it names. It runs only
when every stage before it passed, and reports SKIP no environments declared
for a contract without environments. Environments
lists the rules and the codes it reports: ENVIRONMENT_UNKNOWN_CONFIGURATION,
ENVIRONMENT_VALUES_INVALID, ENVIRONMENT_VALUES_MISSING and the
ENVIRONMENT_VALUES_UNVERIFIED warning.
An error in an environment's file carries that file in file, with line and
column pointing into it, also when --set or -f is given. The stage passes
only when Pacto checked every value set and found no error. A configuration
whose schema is behind a ref leaves its values unchecked, and so does a run
with no bundle files to read, such as the operator loading an inline contract.
A stage with no error and an unchecked value set reports
SKIP could not verify: and names each environment and configuration.