Documentation menu

The CLI collection format

The SchemaClient CLI collection format (.scoll.json): suite shape, expect assertions, captures, variables, data files, and the .tests.json sidecar for app exports.

Updated

The CLI's native collection format is one JSON file — conventionally *.scoll.json — built to be written by hand and reviewed in a pull request. This page is the reference: suite shape, requests, assertions, captures, variables, data files, and the sidecar that carries assertions for desktop-app exports. The behavior here is the normative spec (docs/semantics.md in the CLI repo); when the desktop app gains execution, it will implement these same rules. For installing and running the CLI itself, start with the CLI guide.

The suite

{
  "schemaVersion": 1,
  "name": "Products API",
  "baseUrl": "{{baseUrl}}",
  "variables": { "baseUrl": "https://api.example.com/v2", "apiKey": "demo-key" },
  "defaults": {
    "timeout": 10000,
    "headers": { "Accept": "application/json" },
    "retries": { "attempts": 3, "delayMs": 200, "backoff": 2, "on": ["network", "5xx"] }
  },
  "items": [
    {
      "name": "Products",
      "auth": { "type": "apikey", "name": "X-API-Key", "value": "{{apiKey}}", "location": "header" },
      "items": [
        {
          "name": "List products",
          "method": "GET",
          "url": "/products/",
          "params": { "page_size": "3" },
          "expect": { "status": 200, "contentType": "application/json", "responseTime": 5000, "body": { "$.count": true } },
          "capture": { "firstProductId": "$.results[0].id" },
          "schema": "schemas/product.sc"
        },
        {
          "name": "Get that product",
          "method": "GET",
          "url": "/products/{{firstProductId}}/",
          "expect": { "status": 200, "body": { "$.id": { "matches": "^\\d+$" } } }
        }
      ]
    }
  ]
}

An entry in items that has its own items is a folder; folders nest to any depth. A folder can carry auth (inherited by everything inside it), variables (scoped to that subtree), and "parallel": true to run its requests concurrently. Marking a folder parallel asserts its requests are independent — they all fire at once, so none of them can rely on a sibling's captures.

defaults apply to every request: timeout in milliseconds (30000 when unset), headers merged onto each request, and a retry policy. "retries": 3 is accepted as shorthand for three attempts with the default backoff, retrying on network errors and 5xx.

Requests

method defaults to GET. A relative url joins the suite's baseUrl with exactly one / between them — and any path on the base is preserved:

| baseUrl | url | result | | --- | --- | --- | | https://api.example.com/v2 | /products | https://api.example.com/v2/products | | https://api.example.com | /products | https://api.example.com/products |

This is deliberately not new URL() semantics, which would silently drop the /v2 prefix. A url with its own scheme (https://…) is used verbatim and ignores baseUrl. Entries in params merge onto the URL's query string; a param with the same name as one already in the URL replaces it.

body takes a type of json, raw (with optional contentType), form (multipart, fields may be text or files), urlencoded, binary (a file path), or graphql (query, optional variables and operationName). A bare object with no type is shorthand for JSON.

auth supports basic, bearer, apikey (header or query), and oauth2 client credentials. Resolution walks outward: the request's own auth, then the nearest folder's, then the suite's. {"type": "none"} is an explicit value, not an absence — set it on a request to opt out of inherited auth, which is how you call a public endpoint inside an authenticated folder. A literal Authorization header on the request always beats computed auth.

Requests also take their own timeout, retries, and "disabled": true to skip without deleting. Pacing between requests is a run-level concern — the --delay <ms> flag — not a per-request field.

Assertions: expect

The expect block is a shorthand object; each key becomes one assertion:

| Key | Accepted forms | | --- | --- | | status | 200 · [200, 201] · "2xx" (also 3xx/4xx/5xx) | | contentType | substring of the Content-Type header | | responseTime | maximum milliseconds | | headers | {"X-Id": true} exists · "value" equals · {"matches": "regex"} | | body | {"$.path": true} exists · a value equals · {"matches": "regex"} · {"contains": "substring"} |

Body paths are JSONPath. An explicit array-of-assertions form also exists, but the shorthand covers what suites actually need.

A request with no expect block is not a free pass: a 4xx or 5xx response fails it by default. To assert that an error status is correct, state it — "expect": { "status": 404 }.

Chaining with capture

capture pulls values out of a response into variables for later requests:

"capture": { "token": "$.access_token" }

The shorthand reads from the JSON body. The explicit form reaches headers, cookies, and the status code:

"capture": [
  { "name": "requestId", "from": "header", "path": "X-Request-Id" },
  { "name": "session", "from": "cookie", "path": "sessionid" },
  { "name": "code", "from": "status" }
]

A captured value is available as {{token}} in every subsequent request. Captures run before assertions, so a failing request still reports what it captured; a capture that matches nothing is reported as a problem, not silently skipped.

Variables

Interpolation is {{name}} anywhere in URLs, params, headers, bodies, auth credentials, and assertion values — but not in schema references, because a contract that varies per run is not a contract. Lookup walks a chain where later scopes win: SCHEMACLIENT_VAR_* environment variables (lowest), the config file, suite variables, folder variables, an --env file, the current data-file row, --var flags, captures, then script writes (highest). Two placements are deliberate: ambient environment values rank low so a value written in the collection beats one exported in your shell, and --var beats the data file so an operator can pin a value across all iterations.

Generators are evaluated fresh at each occurrence: {{$uuid}} (alias {{$guid}}), {{$timestamp}} (Unix ms), {{$isoTimestamp}}, and {{$randomInt}} (0 to 999999). Under --seed <n> they become deterministic per iteration, so runs reproduce.

Write \{{name}} for a literal {{name}} — needed when the API's own templating uses the same delimiters. An unresolved variable is an error and the request is not sent, so a typo names itself instead of surfacing as a DNS failure against a host called {{baseurl}}. Relax with --unresolved=warn or ignore if you must.

Data files

-d users.csv (or .json) runs the collection once per row, with each row's columns as variables. CSV columns may declare a type so values are not sent as quoted strings:

username,expected_id:number,active:boolean,meta:json
ada,1,true,"{""role"":""admin""}"
grace,2,false,"{}"

JSON data files are arrays of objects and keep their native types with no annotation. Because assertion values interpolate, a row can carry the input and the expected output:

"expect": { "body": { "$.id": "{{expected_id}}" } }

Schema assertions and the desktop sidecar

A request's schema field validates the response against a schema file — the same engine and .sc language the desktop app uses. The string form names a file; the object form adds options:

"schema": { "file": "schemas/product.sc", "schemaName": "Response", "strict": true }

Collections exported from the desktop app run in the CLI directly — but they cannot hold assertions. The app (and its sync backend) rebuild collections from a fixed field list, so any extra field like expect is silently dropped the next time the collection is opened or synced. Instead, assertions for an exported collection live in a sidecar next to it — collection.json gets collection.tests.json — keyed by item id:

{
  "entries": {
    "req_a1b2c3": {
      "expect": { "status": 200, "body": { "$.id": true } },
      "schema": { "file": "schemas/product.sc" }
    }
  }
}

The sidecar is picked up automatically; --tests <file> points elsewhere and --no-tests ignores it. The app owns the requests, the sidecar owns the CI contract, and neither tool can destroy the other's data. One sharp edge: recreating a request in the app gives it a new id, so its sidecar entry stops matching — the CLI warns about unmatched entries and keeps them in the file rather than discarding your assertions.

Frequently asked questions

What format does the SchemaClient CLI use for collections?

A single JSON file, conventionally named *.scoll.json, with a name, optional baseUrl, variables, defaults, and a nested items tree of folders and requests. Collections exported from the desktop app also run directly — the CLI detects the format from the file's structure, not its extension.

How do I add test assertions to a collection exported from the desktop app?

Put them in a sidecar file named <collection>.tests.json next to the export, keyed by item id. The desktop app rebuilds collections from a fixed field list and would silently drop assertions stored inside the collection file, so the sidecar keeps the app's requests and CI's assertions in separate files that cannot overwrite each other.

Does a request without an expect block always pass?

No. A request with no assertions still fails on a 4xx or 5xx status — there is no pass by omission. If an error status is the expected outcome, say so explicitly with "expect": { "status": 404 } and the run treats it as a pass.