A contract file is a YAML document that declares the resources a participant provides, the resources it consumes, and the schemas of their bodies. Both sides of an integration write the same format. The file holds only the declaration: the participant name and the version are flags of bidirekt publish, never keys in the file.
Example
The provider petstore_api serves one pet by id:
provides: rest: /pets/*: get: responses: 200: Pet schemas: Pet: type: object properties: petId: type: integer name: type: string
The consumer petstore_web reads that pet, and from it only the name:
consumes: petstore_api: rest: /pets/*: get: responses: 200: Pet schemas: Pet: type: object properties: name: type: string
The two files have the same shape. provides holds what this participant serves, and consumes holds, under each provider's name, what it calls. schemas holds the bodies both sides name. The consumer declares only what it reads, so the provider is free to drop petId. All three keys are optional, and one file may carry both provides and consumes: a service in the middle of a chain declares both sides in the same contract.
Resources
A resource is one body the two sides exchange: the request of an endpoint and method, or one response of an endpoint, method and status. A provider declares its resources under provides, in a rest block. A consumer writes the same rest block under consumes, one level deeper, under the name of the provider it calls.
That name is the provider's participant name, in snake_case: lowercase letters, digits and single underscores. It is not checked against existing participants at publish, so a misspelled name, such as petstore_apl for petstore_api, publishes fine and fails at can-i-deploy with petstore_web calls GET /pets/*, but petstore_apl doesn't provide it โ stop calling it, or wait until petstore_apl publishes it.
Each name on the right of request and responses is a schema name declared under schemas, never an inline schema.
Request
provides: rest: /pets: post: request: NewPet
request names the schema of the request body. A body exists only on post and put, so request is accepted on those two methods and nowhere else. A method needs nothing more than its request: the resource above is complete without any responses.
Response
provides: rest: /pets/*: get: responses: 200: Pet
responses maps a status code to the schema of the response body. A status code is an integer from 100 to 599, quoted or not; patterns such as 2xx are rejected.
A request and its responses live together under the same method:
provides: rest: /pets: post: request: NewPet responses: 201: Pet 500: Error
This declares three resources: the request of POST /pets, its 201 response and its 500 response. Each one is compared on its own: a break in the 500 does not involve the 201.
Every status names a schema, even one without a body. A 204 names an object schema with no properties:
provides: rest: /pets/*: delete: responses: 204: NoContent schemas: NoContent: type: object
A status left without a value, such as 204:, is rejected.
Endpoints
The endpoint is part of a resource's identity, so a consumer's endpoint matches a provider's only when both are written the same way:
- โโโAn endpoint starts with
/and has no empty segment./alone is the root. - โโโA dynamic segment is
*, one per segment, with no name:/pets/*,/owners/*/pets/*. - โโโ
{petId}and partial wildcards such asp*are rejected at publish, not rewritten.:petIdis read as a literal segment that no request path matches, so write*. - โโโA trailing slash is removed:
/pets/is/pets, and a provider that writes both has declared the same resources twice. - โโโThe methods are
get,post,putanddelete, in lowercase. Any other key under an endpoint is rejected.
An endpoint copied from an OpenAPI document keeps its named parameter and is rejected:
provides: rest: /pets/{petId}: get: responses: 200: Pet
contract validation failed - petstore_api.yaml: invalid endpoint "/pets/{petId}" at provides rest, dynamic path segments must use *
Written with *, it publishes, and it matches a consumer that also writes /pets/*:
provides: rest: /pets/*: get: responses: 200: Pet
Schemas
schemas: Pet: type: object properties: petId: type: integer name: type: string nickname: type: string optional: true Pets: type: array items: ref: Pet
A node's type is one of object, array, string, integer, float and boolean, and every property may carry optional, true or false. A property is required unless it says optional: true; there is no required list. What optional means for a break depends on which side reads the body, and that rule is in How the broker works. Any node may also carry description, free text that the broker ignores.
Schema names are unique across every file of a publish, and request, responses and ref resolve against all of them.
Primitives
string, integer, float and boolean. There is no number. integer and float are two distinct types compared by exact equality, so a consumer that declares float where the provider declares integer is a type mismatch. Any other token is rejected at publish.
Objects
properties maps each member name to its own node, nested as deep as the body is:
Pet: type: object properties: owner: type: object properties: name: type: string
type: object may be left out when properties is present.
Arrays
items is the node of every element, as in Pets above. An array without items is rejected. type: array may be left out when items is present.
References
ref gives a node the shape of a named schema, at any depth: as a whole schema, as a member, or as the items of an array, like Pets does with Pet. Write ref on its own, with at most optional and description beside it. When a node also carries type, properties or items, that shape wins and the ref is ignored, without a violation:
Pet: type: object properties: owner: type: string ref: Owner
Here owner is a string, and Owner is never read. A ref to a name no schema declares is rejected at publish.
A schema cannot reach itself: a loop such as Pet โ Owner โ Pet is rejected with schema "Pet" is deeper than 10 levels. To describe a recursive body, declare the nested level with only the members that are read, and stop there:
Pet: type: object properties: name: type: string owner: ref: Owner Owner: type: object properties: name: type: string favorite: type: object properties: name: type: string
Owner.favorite is a pet, but it is declared inline with the one member the reader uses, instead of a ref back to Pet.
Several files
bidirekt publish takes any number of files, and all the files of one publish are one contract. The order does not matter, and the shell expands globs such as contracts/*.yaml. Reports name each file by the path you typed.
- โโโOne schema namespace. A schema declared in one file can be named from any other. The same name declared twice is a duplicate. A file that names schemas declared elsewhere is valid only when published together with that file.
- โโโA provider declares each resource once. Files may split the endpoints, the methods or even the statuses of one method between them, but the same resource in two files is a duplicate. So is the same file passed twice. A request has no status, so the
requestof an endpoint and method belongs in exactly one file, even when its responses are spread over several. - โโโA consumer merges by union. Each module of a consumer may declare the resources it reads in its own file. Fragments of the same resource merge: in a response, a property is optional only if every fragment allows it; in a request, it is required only if every fragment sends it. Two fragments that give one property different types are rejected.
A provider that keeps the 201 of POST /pets in one file and its 400 in another writes request in only one of them:
# pets.yaml provides: rest: /pets: post: request: NewPet responses: 201: Pet
# pets_errors.yaml provides: rest: /pets: post: responses: 400: Error
Writing request: NewPet in pets_errors.yaml too declares the request of POST /pets twice:
contract validation failed - pets_errors.yaml: duplicate resource "provides POST /pets request", also declared in pets.yaml
Two modules of the consumer petstore_web read the same GET /pets/*, each its own subset:
# web_list.yaml consumes: petstore_api: rest: /pets/*: get: responses: 200: PetRow schemas: PetRow: type: object properties: petId: type: integer name: type: string
# web_card.yaml consumes: petstore_api: rest: /pets/*: get: responses: 200: PetCard schemas: PetCard: type: object properties: petId: type: integer photoUrl: type: string optional: true
Published together, petstore_web reads petId and name as required and photoUrl as optional, so a provider that returns only petId and name is compatible. Remove optional: true from web_card.yaml and photoUrl becomes required, because one reader needs it.
What is not compared
The grammar has no place for the items below, so a change in any of them never makes can-i-deploy fail.
- โโโHeaders. A provider that starts requiring one, and a consumer that stops sending one, both pass.
- โโโQuery parameters. A renamed parameter, a newly required one, or a change in how pagination is requested is not a break.
- โโโNullability. There is no
nulltype and no way to say that a property may benull. - โโโEnum values. A
stringis astring: a provider that starts returning a fourth value ofstatusis compatible with a consumer that handles three. - โโโFormats. A
date-timestring, an email and free text are the samestring.
File format
- โโโEvery key at every level is checked against the grammar on this page. A key the grammar does not know, such as
patch, an uppercaseGETor a top-levelversion, is a violation. The report lists every violation in every file, and nothing is stored. - โโโThe extension must be
.yamlor.yml. The CLI refuses anything else before contacting the broker:unsupported contract file extension: "petstore_api.txt". - โโโOne YAML document per file. A second document after
---is rejected withmalformed contract file: petstore_api.yaml: multiple documents are not supported. - โโโYAML anchors and aliases are rejected with
malformed contract file: petstore_api.yaml: anchors and aliases are not supported. To reuse a shape, name it underschemasand point to it withref. - โโโYAML comments are allowed. They are ignored when the file is parsed.
How the file is published, and what the broker answers, is in the CLI reference.