Skip to main content

HTTP API

Test the responses of a REST API's GET or POST endpoints. datacontract test calls the endpoints and checks the responses, JSON or YAML, against the contract.

1. Install​

The response is tested with duckdb, so the duckdb extra is required:

uv tool install --python python3.11 --upgrade 'datacontract-cli[duckdb]'

See Installation for pip, pipx, and Docker.

2. Authenticate​

If the API requires authentication, set the value for the Authorization header, or the API key for the X-API-Key header:

# .env
DATACONTRACT_API_HEADER_AUTHORIZATION="Bearer <token>"
DATACONTRACT_API_HEADER_X_API_KEY="<api-key>"

For public endpoints, skip this step.

3. Create a contract​

From the OpenAPI document​

If the API has an OpenAPI 3.x document, import the operation you want to test, by its operationId or its method and path (POST /orders/search):

datacontract import openapi --source openapi.yaml --operation listOrders --output datacontract.yaml

The contract gets one schema for the operation's response, and an api server for each server in the document, located at the endpoint:

servers:
- server: production
type: api
description: Production
customProperties:
- property: params
value:
limit: ${limit:-100}
location: https://api.example.com/v1/orders
- server: staging
type: api
description: Staging
customProperties:
- property: params
value:
limit: ${limit:-100}
location: https://staging.api.example.com/v1/orders
schema:
- name: listOrders
# ...

To test several endpoints of the API in one run, repeat --operation, or run the command in a terminal without --operation to pick the operations from a list:

datacontract import openapi --source openapi.yaml --operation listCustomers --operation listOrders --output datacontract.yaml

Each operation becomes a schema, served by its own endpoint (see Testing several endpoints).

Path parameters become variables in the location, and required query parameters variables in params, defaulting to the parameter's example or default. Set them in the environment to call the endpoint with other values, for example limit=10 datacontract test datacontract.yaml. See Import: OpenAPI for what is imported.

From a response​

Without an OpenAPI document, fetch one response and import its schema:

curl -o response.json https://api.example.com/orders
datacontract import json --source response.json --output datacontract.yaml

The import generates a servers entry of type: local. Replace it with the API endpoint:

servers:
- server: api
type: api
location: "https://api.example.com/orders"

Testing POST requests​

Custom properties of the server set the method, the query parameters and the JSON body of the request. For a search API that only accepts POST:

servers:
- server: search
type: api
location: "https://api.example.com/orders/search"
customProperties:
- property: method
value: POST
- property: params
value:
limit: ${limit:-100}
status: [placed, shipped]
- property: body
value:
query:
customer_id: ${customer_id:-C-1}
size: 100

This sends a POST request with the query parameters limit=100, status=placed and status=shipped, and the body {"query": {"customer_id": "C-1"}, "size": 100}. See the reference for the rules.

Testing several endpoints​

When the schemas of a contract are served by different endpoints of the same API, locate the server at the API's base URL, and give each schema an endpoint custom property with its path below the location. Each endpoint is called, and its response is tested against its schema. Relationships between the schemas are checked across the endpoints, such as that every order references an existing customer:

servers:
- server: production
type: api
location: "https://api.example.com/v1"
schema:
- name: customers
customProperties:
- property: endpoint
value: /customers
properties:
- name: id
logicalType: string
unique: true
- name: orders
customProperties:
- property: endpoint
value: /orders
- property: params
value:
status: ${status:-open}
properties:
- name: order_id
logicalType: string
- name: customer_id
logicalType: string
relationships:
- to: customers.id

The method, params and body of an endpoint are custom properties of its schema, not of the server. Every schema needs an endpoint. With --schema-name, only that schema's endpoint is called, so its relationships to other schemas are not checked.

4. Test the actual data​

datacontract test datacontract.yaml

The first server is tested unless you select one with --server, e.g. --server staging.

Testing datacontract.yaml
Server: production (type=api, location=https://api.example.com/v1/orders)
╭────────┬─────────────────────────────────────────────────┬──────────┬─────────╮
│ Result │ Check │ Field │ Details │
├────────┼─────────────────────────────────────────────────┼──────────┼─────────┤
│ ... │ │ │ │
│ passed │ Check that field 'order_id' is present │ order_id │ │
│ passed │ Check that field order_id has no missing values │ order_id │ │
│ ... │ │ │ │
╰────────┴─────────────────────────────────────────────────┴──────────┴─────────╯
🟢 Data contract is valid. Ran 10 checks. Took 1.2 seconds.

The request asks for JSON or YAML (Accept: application/json, application/yaml). How the response is read depends on its Content-Type:

  • JSON: a single object, an array of records, or one record per line (JSON Lines) is detected automatically.
  • YAML (application/yaml, application/x-yaml, text/yaml): the YAML is read as the JSON it holds and then tested the same way. A stream of several YAML documents (separated by ---) counts as one record per document.

5. Let it catch a violation​

The contract becomes valuable when it detects drift. Tighten an expectation — for example, mark a field as required: true, restrict a status field to its allowed values, or add a quality rule. Run datacontract test datacontract.yaml again: every violation is listed as an error, and the command exits with code 1 — ready for CI/CD and scheduled runs so you catch drift before your consumers do.

Reference​

Authentication options and data type handling: HTTP API Reference.

Troubleshooting​

  • 401 / 403: set DATACONTRACT_API_HEADER_AUTHORIZATION including the scheme (e.g. Bearer eyJ...), not just the raw token, or DATACONTRACT_API_HEADER_X_API_KEY if the API expects an X-API-Key header.
  • Variable orderId referenced in ... is not set: the endpoint has a path or query parameter without an example or default. Set it in the environment or a .env file, or write a default into the location (${orderId:-ORD-1001}).
  • Schema checks fail on a wrapped response: if the API returns {"data": [...]} instead of a plain array, model the wrapper object in the schema, or set delimiter accordingly (array for a JSON array of records, none for a single object).